From 87a34c56961b109ab45648e5f9fa055954249a22 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Mon, 29 May 2023 13:43:57 +0100 Subject: [PATCH 01/52] Update quick start and fetching data guide to v2-sdk --- docs/sdk/v2/guides/01-quick-start.md | 12 ++-- docs/sdk/v2/guides/02-fetching-data.md | 88 ++++++-------------------- 2 files changed, 26 insertions(+), 74 deletions(-) diff --git a/docs/sdk/v2/guides/01-quick-start.md b/docs/sdk/v2/guides/01-quick-start.md index 5f3ffa1e29..5f8134040a 100644 --- a/docs/sdk/v2/guides/01-quick-start.md +++ b/docs/sdk/v2/guides/01-quick-start.md @@ -7,7 +7,7 @@ The Uniswap SDK exists to help developers build on top of Uniswap. It's designed # Installation -The easiest way to consume the SDK is via [npm](https://github.com/Uniswap/uniswap-v2-sdk). To install it in your project, simply run `yarn add @uniswap/sdk` (or `npm install @uniswap/sdk`). +The easiest way to consume the SDK is via [npm](https://github.com/Uniswap/uniswap-v2-sdk). To install it in your project, simply run `yarn add @uniswap/v2-sdk` (or `npm install @uniswap/v2-sdk`). This also installs the sdk-core package that is used by both the V2 and V3 SDK and ethers as dependencies. # Usage @@ -16,15 +16,17 @@ To run code from the SDK in your application, use an `import` or `require` state ## ES6 (import) ```typescript -import { ChainId } from '@uniswap/sdk' -console.log(`The chainId of mainnet is ${ChainId.MAINNET}.`) +import { SupportedChainId } from '@uniswap/sdk-core' +import {Pair} from '@uniswap/v2-sdk' +console.log(`The chainId of mainnet is ${SupportedChainId.MAINNET}.`) ``` ## CommonJS (require) ```typescript -const UNISWAP = require('@uniswap/sdk') -console.log(`The chainId of mainnet is ${UNISWAP.ChainId.MAINNET}.`) +const CORE = require('@uniswap/sdk-core') +const V2_SDK = require('@uniswap/v2-sdk') +console.log(`The chainId of mainnet is ${CORE.SupportedChainId.MAINNET}.`) ``` # Reference diff --git a/docs/sdk/v2/guides/02-fetching-data.md b/docs/sdk/v2/guides/02-fetching-data.md index 60d02159aa..41c0f7504c 100644 --- a/docs/sdk/v2/guides/02-fetching-data.md +++ b/docs/sdk/v2/guides/02-fetching-data.md @@ -6,7 +6,7 @@ title: Fetching Data > Looking for a [quickstart](quick-start)? While the SDK is fully self-contained, there are two cases where it needs _on-chain data_ to function. -This guide will detail both of these cases, and offer some strategies that you can use to fetch this data. +This guide will detail both of these cases, and offer a sample that you can use to fetch this data. # Case 1: Tokens @@ -29,9 +29,9 @@ The next piece of data we need is **decimals**. One option here is to simply pass in the correct value, which we may know is `18`. At this point, we're ready to represent DAI as a [Token](../reference/token): ```typescript -import { ChainId, Token } from '@uniswap/sdk' +import { SupportedChainId, Token } from '@uniswap/sdk-core' -const chainId = ChainId.MAINNET +const chainId = SupportedChainId.MAINNET const tokenAddress = '0x6B175474E89094C44Da98b954EedeAC495271d0F' // must be checksummed const decimals = 18 @@ -41,58 +41,25 @@ const DAI = new Token(chainId, tokenAddress, decimals) If we don't know or don't want to hardcode the value, we could look it up ourselves via any method of retrieving on-chain data in a function that looks something like: ```typescript -import { ChainId } from '@uniswap/sdk' +import { SupportedChainId } from '@uniswap/sdk-core' -async function getDecimals(chainId: ChainId, tokenAddress: string): Promise { - // implementation details +async function getDecimals(chainId: SupportedChainId, tokenAddress: string): Promise { + // Setup provider, import necessary ABI ... + const tokenContract = new ethers.Contract(tokenAddress, erc20abi, provider) + return tokenContract["decimals"]() } ``` -### Fetched by the SDK - -If we don't want to provide or look up the value ourselves, we can ask the SDK to look it up for us with [Fetcher.fetchTokenData](../reference/fetcher#fetchtokendata) - -```typescript -import { ChainId, Token, Fetcher } from '@uniswap/sdk' - -const chainId = ChainId.MAINNET -const tokenAddress = '0x6B175474E89094C44Da98b954EedeAC495271d0F' // must be checksummed - -// note that you may want/need to handle this async code differently, -// for example if top-level await is not an option -const DAI: Token = await Fetcher.fetchTokenData(chainId, tokenAddress) -``` - -By default, this method will use the [default provider defined by ethers.js](https://docs.ethers.io/v5/api/providers/#providers-getDefaultProvider). -If you're already using ethers.js in your application, you may pass in your provider as a 3rd argument. -If you're using another library, you'll have to fetch the data separately. - ## Optional Data Finally, we can talk about **symbol** and **name**. Because these fields aren't used anywhere in the SDK itself, they're optional, and can be provided if you want to use them in your application. However, the SDK will not fetch them for you, so you'll have to provide them: ```typescript -import { ChainId, Token } from '@uniswap/sdk' +import { SupportedChainId, Token } from '@uniswap/sdk-core' const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin') ``` -or: - -```typescript -import { ChainId, Token, Fetcher } from '@uniswap/sdk' - -// note that you may want/need to handle this async code differently, -// for example if top-level await is not an option -const DAI = await Fetcher.fetchTokenData( - ChainId.MAINNET, - '0x6B175474E89094C44Da98b954EedeAC495271d0F', - undefined, - 'DAI', - 'Dai Stablecoin' -) -``` - # Case 2: Pairs Now that we've explored how to define a token, let's talk about pairs. To read more about what Uniswap pairs are, see [Pair](../../../contracts/v2/reference/smart-contracts/pair) @@ -101,7 +68,7 @@ As an example, let's try to represent the DAI-WETH pair. ## Identifying Data -Each pair consists of two tokens (see previous section). Note that WETH used by the router is [exported by the SDK](../reference/other-exports). +Each pair consists of two tokens (see previous section). Note that WETH used by the router is [exported by the SDK Core as WETH9](../../core/reference/overview.md). ## Required Data @@ -109,45 +76,28 @@ The data we need is the _reserves_ of the pair. To read more about reserves, see ### Provided by the User -One option here is to simply pass in values which we've fetched ourselves to create a [Pair](../reference/pair): +One option here is to simply pass in values which we've fetched ourselves to create a [Pair](../reference/pair). In this example we use ethers to fetch the data directly from the blockchain: ```typescript -import { ChainId, Token, WETH, Pair, TokenAmount } from '@uniswap/sdk' +import { SupportedChainId, Token, WETH9, CurrencyAmount } from '@uniswap/sdk-core' +import { Pair } from '@uniswap/v2-sdk' const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) async function getPair(): Promise { - const pairAddress = Pair.getAddress(DAI, WETH[DAI.chainId]) + const pairAddress = Pair.getAddress(DAI, WETH9[DAI.chainId]) - const reserves = [ - /* use pairAddress to fetch reserves here */ - ] + // Setup provider, import necessary ABI ... + const pairContract = new ethers.Contract(pairAddress, uniswapV2poolABI, provider) + const reserves = await pairContract["getReserves"]() const [reserve0, reserve1] = reserves - const tokens = [DAI, WETH[DAI.chainId]] + const tokens = [DAI, WETH9[DAI.chainId]] const [token0, token1] = tokens[0].sortsBefore(tokens[1]) ? tokens : [tokens[1], tokens[0]] - const pair = new Pair(new TokenAmount(token0, reserve0), new TokenAmount(token1, reserve1)) + const pair = new Pair(CurrencyAmount.fromRawAmount(token0, reserve0), CurrencyAmount.fromRawAmount(token1, reserve1)) return pair } ``` Note that these values can change as frequently as every block, and should be kept up-to-date. - -### Fetched by the SDK - -If we don't want to look up the value ourselves, we can ask the SDK to look them up for us with [Fetcher.fetchTokenData](../reference/fetcher#fetchtokendata): - -```typescript -import { ChainId, Token, WETH, Fetcher } from '@uniswap/sdk' - -const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) - -// note that you may want/need to handle this async code differently, -// for example if top-level await is not an option -const pair = await Fetcher.fetchPairData(DAI, WETH[DAI.chainId]) -``` - -By default, this method will use the [default provider defined by ethers.js](https://docs.ethers.io/v5/api/providers/#providers-getDefaultProvider). If you're already using ethers.js in your application, you may pass in your provider as a 3rd argument. If you're using another library, you'll have to fetch the data separately. - -Note that these values can change as frequently as every block, and should be kept up-to-date. From 8894b50cdf3b3da6d63f4374355fd74d783959bd Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Mon, 29 May 2023 19:07:29 +0100 Subject: [PATCH 02/52] Update V2 pricing guide --- docs/sdk/v2/guides/03-pricing.md | 59 +++++++++++++++----------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/docs/sdk/v2/guides/03-pricing.md b/docs/sdk/v2/guides/03-pricing.md index 672cff64b0..dc972eebaf 100644 --- a/docs/sdk/v2/guides/03-pricing.md +++ b/docs/sdk/v2/guides/03-pricing.md @@ -18,45 +18,45 @@ Let's consider the mid price for DAI-WETH (that is, the amount of DAI per 1 WETH The simplest way to get the DAI-WETH mid price is to observe the pair directly: ```typescript -import { ChainId, Token, WETH, Fetcher, Route } from '@uniswap/sdk' +import { SupportedChainId, Token, WETH9 } from '@uniswap/sdk-core' +import { Route } from '@uniswap/v2-sdk' -const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) +const DAI = new Token(SupportedChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) -// note that you may want/need to handle this async code differently, -// for example if top-level await is not an option -const pair = await Fetcher.fetchPairData(DAI, WETH[DAI.chainId]) +// To learn how to get Pair data, refer to the previous guide. +const pair = await getPair(DAI, WETH9[SupportedChainId.MAINNET]) -const route = new Route([pair], WETH[DAI.chainId]) +const route = new Route([pair], WETH9[DAI.chainId], DAI) -console.log(route.midPrice.toSignificant(6)) // 201.306 -console.log(route.midPrice.invert().toSignificant(6)) // 0.00496756 +console.log(route.midPrice.toSignificant(6)) // 1901.08 +console.log(route.midPrice.invert().toSignificant(6)) // 0.000526017 ``` -You may be wondering why we have to construct a _route_ to get the mid price, as opposed to simply getting it from the pair (which, after all, includes all the necessary data). The reason is simple: a route forces us to be opinionated about the _direction_ of trading. Routes consist of one or more pairs, and an input token (which fully defines a trading path). In this case, we passed WETH as the input token, meaning we're interested in a WETH -> DAI trade. +You may be wondering why we have to construct a _route_ to get the mid price, as opposed to simply getting it from the pair (which, after all, includes all the necessary data). The reason is simple: a route forces us to be opinionated about the _direction_ of trading. Routes consist of one or more pairs, an input token and an output token (which fully defines a trading path). In this case, we passed WETH as the input token and DAI as the output token, meaning we're interested in a WETH -> DAI trade. Now we understand that the mid price is going to be defined in terms of DAI/WETH. Not to worry though, if we need the WETH/DAI price, we can easily invert. -Finally, you may have noticed that we're formatting the price to 6 significant digits. This is because internally, prices are stored as exact-precision fractions, which can be converted to other representations on demand. For a full list of options, see [Price](../reference/fractions#price). +Finally, you may have noticed that we're formatting the price to 6 significant digits. This is because internally, prices are stored as exact-precision fractions, which can be converted to other representations on demand. For a full list of options, see [Price](../../core/reference/classes/Price.md). ## Indirect For the sake of example, let's imagine a direct pair between DAI and WETH _doesn't exist_. In order to get a DAI-WETH mid price we'll need to pick a valid route. Imagine both DAI and WETH have pairs with a third token, USDC. In that case, we can calculate an indirect mid price through the USDC pairs: ```typescript -import { ChainId, Token, WETH, Fetcher, Route } from '@uniswap/sdk' +import { SupportedChainId, Token, WETH9} from '@uniswap/sdk-core' +import { Route, Pair } from '@uniswap/v2-sdk' -const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6) -const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) +const USDC = new Token(SupportedChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6) +const DAI = new Token(SupportedChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) -// note that you may want/need to handle this async code differently, -// for example if top-level await is not an option -const USDCWETHPair = await Fetcher.fetchPairData(USDC, WETH[ChainId.MAINNET]) -const DAIUSDCPair = await Fetcher.fetchPairData(DAI, USDC) +// To learn how to get Pair data, refer to the previous guide. +const USDCWETHPair = await getPair(USDC, WETH9[SupportedChainId.MAINNET]) +const DAIUSDCPair = await getPair(DAI, USDC) -const route = new Route([USDCWETHPair, DAIUSDCPair], WETH[ChainId.MAINNET]) +const route = new Route([USDCWETHPair, DAIUSDCPair], WETH9[SupportedChainId.MAINNET], DAI) -console.log(route.midPrice.toSignificant(6)) // 202.081 -console.log(route.midPrice.invert().toSignificant(6)) // 0.00494851 +console.log(route.midPrice.toSignificant(6)) // 1896.34 +console.log(route.midPrice.invert().toSignificant(6)) // 0.000527331 ``` # Execution Price @@ -66,22 +66,19 @@ Mid prices are great representations of the _current_ state of a route, but what Imagine we're interested in trading 1 WETH for DAI: ```typescript -import { ChainId, Token, WETH, Fetcher, Trade, Route, TokenAmount, TradeType } from '@uniswap/sdk' +import { SupportedChainId, Token, WETH9, CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { Route, Pair, Trade } from '@uniswap/v2-sdk' -const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) +const DAI = new Token(SupportedChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) -// note that you may want/need to handle this async code differently, -// for example if top-level await is not an option -const pair = await Fetcher.fetchPairData(DAI, WETH[DAI.chainId]) +// To learn how to get Pair data, refer to the previous guide. +const pair = await getPair(DAI, WETH9[DAI.chainId]) -const route = new Route([pair], WETH[DAI.chainId]) +const route = new Route([pair], WETH9[DAI.chainId], DAI) -const trade = new Trade(route, new TokenAmount(WETH[DAI.chainId], '1000000000000000000'), TradeType.EXACT_INPUT) +const trade = new Trade(route, CurrencyAmount.fromRawAmount(WETH9[DAI.chainId], '1000000000000000000'), TradeType.EXACT_INPUT) -console.log(trade.executionPrice.toSignificant(6)) -console.log(trade.nextMidPrice.toSignificant(6)) +console.log(trade.executionPrice.toSignificant(6)) // 1894.91 ``` Notice that we're constructing a trade of 1 WETH for as much DAI as possible, _given the current reserves of the direct pair_. The execution price represents the average DAI/WETH price for this trade. Of course, the reserves of any pair can change every block, which would affect the execution price. - -Also notice that we're able to access the _next_ mid price, if the trade were to complete successfully before the reserves changed. From 04e190f04778e5d8cf3d12025ca59076b44fdfb0 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Mon, 29 May 2023 19:14:48 +0100 Subject: [PATCH 03/52] Update V2 trading guide --- docs/sdk/v2/guides/04-trading.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/sdk/v2/guides/04-trading.md b/docs/sdk/v2/guides/04-trading.md index 7706a90712..a37031364d 100644 --- a/docs/sdk/v2/guides/04-trading.md +++ b/docs/sdk/v2/guides/04-trading.md @@ -7,26 +7,26 @@ title: Trading The SDK _cannot execute trades or send transactions on your behalf_. Rather, it offers utility classes and functions which make it easy to calculate the data required to safely interact with Uniswap. Nearly everything you need to safely transact with Uniswap is provided by the [Trade](../reference/trade) entity. However, it is your responsibility to use this data to send transactions in whatever context makes sense for your application. -This guide will focus exclusively on sending a transaction to the [currently recommended Uniswap router](../../../contracts/v2/reference/smart-contracts/router-02) +This guide will focus exclusively on sending a transaction to the [latest Uniswap V2 router](../../../contracts/v2/reference/smart-contracts/router-02) # Sending a Transaction to the Router Let's say we want to trade 1 WETH for as much DAI as possible: ```typescript -import { ChainId, Token, WETH, Fetcher, Trade, Route, TokenAmount, TradeType } from '@uniswap/sdk' +import { SupportedChainId, Token, WETH9, CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import {Trade, Route} from '@uniswap/v2-sdk' -const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) +const DAI = new Token(SupportedChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) -// note that you may want/need to handle this async code differently, -// for example if top-level await is not an option -const pair = await Fetcher.fetchPairData(DAI, WETH[DAI.chainId]) +// See the Fetching Data guide to learn how to get Pair data +const pair = await getPair(DAI, WETH9[DAI.chainId]) -const route = new Route([pair], WETH[DAI.chainId]) +const route = new Route([pair], WETH9[DAI.chainId], DAI) const amountIn = '1000000000000000000' // 1 WETH -const trade = new Trade(route, new TokenAmount(WETH[DAI.chainId], amountIn), TradeType.EXACT_INPUT) +const trade = new Trade(route, CurrencyAmount.fromRawAmount(WETH9[DAI.chainId], amountIn), TradeType.EXACT_INPUT) ``` So, we've constructed a trade entity, but how do we use it to actually send a transaction? There are still a few pieces we need to put in place. @@ -47,15 +47,15 @@ function swapExactETHForTokens(uint amountOutMin, address[] calldata path, addre Jumping back to our trading code, we can construct all the necessary parameters: ```typescript -import { Percent } from '@uniswap/sdk' +import {Percent} from '@uniswap/sdk-core' const slippageTolerance = new Percent('50', '10000') // 50 bips, or 0.50% -const amountOutMin = trade.minimumAmountOut(slippageTolerance).raw // needs to be converted to e.g. hex -const path = [WETH[DAI.chainId].address, DAI.address] +const amountOutMin = trade.minimumAmountOut(slippageTolerance).toExact() // needs to be converted to e.g. decimal string +const path = [WETH9[DAI.chainId].address, DAI.address] const to = '' // should be a checksummed recipient address const deadline = Math.floor(Date.now() / 1000) + 60 * 20 // 20 minutes from the current Unix time -const value = trade.inputAmount.raw // // needs to be converted to e.g. hex +const value = trade.inputAmount.toExact() // // needs to be converted to e.g. decimal string ``` The slippage tolerance encodes _how large of a price movement we're willing to tolerate before our trade will fail to execute_. Since Ethereum transactions are broadcast and confirmed in an adversarial environment, this tolerance is the best we can do to protect ourselves against price movements. We use this slippage tolerance to calculate the _minumum_ amount of DAI we must receive before our trade reverts, thanks to [minimumAmountOut](../reference/trade#minimumamountout-since-204). Note that this code calculates this worst-case outcome _assuming that the current price, i.e the route's mid price,_ is fair (usually a good assumption because of arbitrage). From 1ac03a849982b453314b78097be93f7c62064739 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Mon, 29 May 2023 19:17:12 +0100 Subject: [PATCH 04/52] Rename getPair to createPair to avoid confusion with sdk function --- docs/sdk/v2/guides/02-fetching-data.md | 2 +- docs/sdk/v2/guides/03-pricing.md | 8 ++++---- docs/sdk/v2/guides/04-trading.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/sdk/v2/guides/02-fetching-data.md b/docs/sdk/v2/guides/02-fetching-data.md index 41c0f7504c..492f5938c5 100644 --- a/docs/sdk/v2/guides/02-fetching-data.md +++ b/docs/sdk/v2/guides/02-fetching-data.md @@ -84,7 +84,7 @@ import { Pair } from '@uniswap/v2-sdk' const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) -async function getPair(): Promise { +async function createPair(): Promise { const pairAddress = Pair.getAddress(DAI, WETH9[DAI.chainId]) // Setup provider, import necessary ABI ... diff --git a/docs/sdk/v2/guides/03-pricing.md b/docs/sdk/v2/guides/03-pricing.md index dc972eebaf..6ac5a2e169 100644 --- a/docs/sdk/v2/guides/03-pricing.md +++ b/docs/sdk/v2/guides/03-pricing.md @@ -24,7 +24,7 @@ import { Route } from '@uniswap/v2-sdk' const DAI = new Token(SupportedChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) // To learn how to get Pair data, refer to the previous guide. -const pair = await getPair(DAI, WETH9[SupportedChainId.MAINNET]) +const pair = await createPair(DAI, WETH9[SupportedChainId.MAINNET]) const route = new Route([pair], WETH9[DAI.chainId], DAI) @@ -50,8 +50,8 @@ const USDC = new Token(SupportedChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9E const DAI = new Token(SupportedChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) // To learn how to get Pair data, refer to the previous guide. -const USDCWETHPair = await getPair(USDC, WETH9[SupportedChainId.MAINNET]) -const DAIUSDCPair = await getPair(DAI, USDC) +const USDCWETHPair = await createPair(USDC, WETH9[SupportedChainId.MAINNET]) +const DAIUSDCPair = await createPair(DAI, USDC) const route = new Route([USDCWETHPair, DAIUSDCPair], WETH9[SupportedChainId.MAINNET], DAI) @@ -72,7 +72,7 @@ import { Route, Pair, Trade } from '@uniswap/v2-sdk' const DAI = new Token(SupportedChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) // To learn how to get Pair data, refer to the previous guide. -const pair = await getPair(DAI, WETH9[DAI.chainId]) +const pair = await createPair(DAI, WETH9[DAI.chainId]) const route = new Route([pair], WETH9[DAI.chainId], DAI) diff --git a/docs/sdk/v2/guides/04-trading.md b/docs/sdk/v2/guides/04-trading.md index a37031364d..7a4338bb4f 100644 --- a/docs/sdk/v2/guides/04-trading.md +++ b/docs/sdk/v2/guides/04-trading.md @@ -20,7 +20,7 @@ import {Trade, Route} from '@uniswap/v2-sdk' const DAI = new Token(SupportedChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) // See the Fetching Data guide to learn how to get Pair data -const pair = await getPair(DAI, WETH9[DAI.chainId]) +const pair = await createPair(DAI, WETH9[DAI.chainId]) const route = new Route([pair], WETH9[DAI.chainId], DAI) From 14d1d7b5ec90c14e3170fa1b4dc8ed247f1d6037 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Mon, 29 May 2023 19:22:28 +0100 Subject: [PATCH 05/52] Update getting pair address example --- docs/sdk/v2/guides/05-getting-pair-addresses.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sdk/v2/guides/05-getting-pair-addresses.md b/docs/sdk/v2/guides/05-getting-pair-addresses.md index 550d68c899..89cf732693 100644 --- a/docs/sdk/v2/guides/05-getting-pair-addresses.md +++ b/docs/sdk/v2/guides/05-getting-pair-addresses.md @@ -29,10 +29,10 @@ Thanks to some [fancy footwork in the factory](https://github.com/Uniswap/uniswa ### TypeScript -This example makes use of the [Uniswap SDK](../reference/getting-started). In reality, the SDK computes pair addresses behind the scenes, obviating the need to compute them manually like this. +This example makes use of the [Uniswap V2 SDK](../reference/getting-started). In reality, the SDK computes pair addresses behind the scenes, obviating the need to compute them manually like this. ```typescript -import { FACTORY_ADDRESS, INIT_CODE_HASH } from '@uniswap/sdk' +import { FACTORY_ADDRESS, INIT_CODE_HASH } from '@uniswap/v2-sdk' import { pack, keccak256 } from '@ethersproject/solidity' import { getCreate2Address } from '@ethersproject/address' From a5ca8d12c27b4203d63e62e670f30e9ac92e80fa Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Tue, 30 May 2023 15:34:37 +0100 Subject: [PATCH 06/52] Update V2 Technical reference from uniswap-sdk to V2 sdk --- docs/sdk/v2/reference/01-getting-started.md | 12 +- .../v2/reference/{03-pair.md => 02-pair.md} | 29 +- docs/sdk/v2/reference/02-token.md | 70 ----- docs/sdk/v2/reference/03-route.md | 65 +++++ docs/sdk/v2/reference/04-route.md | 64 ---- .../v2/reference/{05-trade.md => 04-trade.md} | 87 +++--- docs/sdk/v2/reference/05-other-exports.md | 42 +++ docs/sdk/v2/reference/06-fractions.md | 276 ------------------ docs/sdk/v2/reference/07-fetcher.md | 39 --- docs/sdk/v2/reference/08-other-exports.md | 106 ------- 10 files changed, 178 insertions(+), 612 deletions(-) rename docs/sdk/v2/reference/{03-pair.md => 02-pair.md} (69%) delete mode 100644 docs/sdk/v2/reference/02-token.md create mode 100644 docs/sdk/v2/reference/03-route.md delete mode 100644 docs/sdk/v2/reference/04-route.md rename docs/sdk/v2/reference/{05-trade.md => 04-trade.md} (52%) create mode 100644 docs/sdk/v2/reference/05-other-exports.md delete mode 100644 docs/sdk/v2/reference/06-fractions.md delete mode 100644 docs/sdk/v2/reference/07-fetcher.md delete mode 100644 docs/sdk/v2/reference/08-other-exports.md diff --git a/docs/sdk/v2/reference/01-getting-started.md b/docs/sdk/v2/reference/01-getting-started.md index 40e60ff9d6..02eabd3598 100644 --- a/docs/sdk/v2/reference/01-getting-started.md +++ b/docs/sdk/v2/reference/01-getting-started.md @@ -23,6 +23,9 @@ The second concern is precision loss due to, for example, chained price ratio ca To address this issue, all math operations are performed as fraction operations, ensuring arbitrary precision up until the point that values are rounded for display purposes, or truncated to fit inside a fixed bit width. +The Fractions class, among others that the V2 SDK depends on, are exported from the SDK Core to allow interoperability with the V3 SDK. +Refer to the [Core SDK section of the docs](../../core/overview.md) to learn more about these classes. + The SDK works for all chains on which the [factory](../../../contracts/v2/reference/smart-contracts/factory#address) is deployed. ## Code @@ -31,10 +34,5 @@ The [source code is available on GitHub](https://github.com/Uniswap/uniswap-sdk) ## Dependencies -The SDK declares its dependencies as [peer dependencies](https://github.com/Uniswap/uniswap-sdk/blob/v2/package.json#L33). -This is for two reasons: - -- prevent installation of unused dependencies (e.g. `@ethersproject/providers` and `@ethersproject/contracts`, only used in [`Fetcher`](fetcher)) -- prevent duplicate `@ethersproject` dependencies with conflicting versions - -However, this means you must install these dependencies alongside the SDK, if you do not already have them installed. +The SDK installs a small number of dependencies(https://github.com/Uniswap/v2-sdk/blob/main/package.json#L24). +The most important dependency of the V2 SDK is the SDK core, which was previously part of the V2 SDK itself, but later released as its own package to avoid duplicate code between the V2 and V3 SDK. diff --git a/docs/sdk/v2/reference/03-pair.md b/docs/sdk/v2/reference/02-pair.md similarity index 69% rename from docs/sdk/v2/reference/03-pair.md rename to docs/sdk/v2/reference/02-pair.md index c764f06d0e..8e9deee3be 100644 --- a/docs/sdk/v2/reference/03-pair.md +++ b/docs/sdk/v2/reference/02-pair.md @@ -4,7 +4,7 @@ title: Pair --- ```typescript -constructor(tokenAmountA: TokenAmount, tokenAmountB: TokenAmount) +constructor(tokenAmountA: CurrencyAmount, tokenAmountB: CurrencyAmount) ``` The Pair entity represents a Uniswap pair with a balance of each of its pair tokens. @@ -12,12 +12,13 @@ The Pair entity represents a Uniswap pair with a balance of each of its pair tok # Example ```typescript -import { ChainId, Token, TokenAmount, Pair } from '@uniswap/sdk' +import { Pair } from '@uniswap/sdk-core' +import {SupportedChainId, Token, CurrencyAmount } from '@uniswap/v2-sdk' -const HOT = new Token(ChainId.MAINNET, '0xc0FFee0000000000000000000000000000000000', 18, 'HOT', 'Caffeine') -const NOT = new Token(ChainId.MAINNET, '0xDeCAf00000000000000000000000000000000000', 18, 'NOT', 'Caffeine') +const HOT = new Token(SupportedChainId.MAINNET, '0xc0FFee0000000000000000000000000000000000', 18, 'HOT', 'Caffeine') +const NOT = new Token(SupportedChainId.MAINNET, '0xDeCAf00000000000000000000000000000000000', 18, 'NOT', 'Caffeine') -const pair = new Pair(new TokenAmount(HOT, '2000000000000000000'), new TokenAmount(NOT, '1000000000000000000')) +const pair = new Pair(CurrencyAmount.fromRawAmount(HOT, '2000000000000000000'), CurrencyAmount.fromRawAmount(NOT, '1000000000000000000')) ``` # Static Methods @@ -59,7 +60,7 @@ See [Token1](../../../contracts/v2/reference/smart-contracts/pair#token1). ## reserve0 ```typescript -reserve0: TokenAmount +reserve0: CurrencyAmount ``` The reserve of token0. @@ -67,7 +68,7 @@ The reserve of token0. ## reserve1 ```typescript -reserve1: TokenAmount +reserve1: CurrencyAmount ``` The reserve of token1. @@ -77,7 +78,7 @@ The reserve of token1. ## reserveOf ```typescript -reserveOf(token: Token): TokenAmount +reserveOf(token: Token): CurrencyAmount ``` Returns reserve0 or reserve1, depending on whether token0 or token1 is passed in. @@ -85,7 +86,7 @@ Returns reserve0 or reserve1, depending on whether token0 or token1 is passed in ## getOutputAmount ```typescript -getOutputAmount(inputAmount: TokenAmount): [TokenAmount, Pair] +getOutputAmount(inputAmount: CurrencyAmount): [CurrencyAmount, Pair] ``` Pricing function for exact input amounts. Returns maximum output amount based on current reserves and the new Pair that would exist if the trade were executed. @@ -93,7 +94,7 @@ Pricing function for exact input amounts. Returns maximum output amount based on ## getInputAmount ```typescript -getInputAmount(outputAmount: TokenAmount): [TokenAmount, Pair] +getInputAmount(outputAmount: CurrencyAmount): [CurrencyAmount, Pair] ``` Pricing function for exact output amounts. Returns minimum input amount based on current reserves and the new Pair that would exist if the trade were executed. @@ -101,7 +102,7 @@ Pricing function for exact output amounts. Returns minimum input amount based on ## getLiquidityMinted ```typescript -getLiquidityMinted(totalSupply: TokenAmount, tokenAmountA: TokenAmount, tokenAmountB: TokenAmount): TokenAmount +getLiquidityMinted(totalSupply: CurrencyAmount, tokenAmountA: CurrencyAmount, tokenAmountB: CurrencyAmount): CurrencyAmount ``` Calculates the exact amount of liquidity tokens minted from a given amount of token0 and token1. @@ -114,11 +115,11 @@ Calculates the exact amount of liquidity tokens minted from a given amount of to ```typescript getLiquidityValue( token: Token, - totalSupply: TokenAmount, - liquidity: TokenAmount, + totalSupply: CurrencyAmount, + liquidity: CurrencyAmount, feeOn: boolean = false, kLast?: BigintIsh -): TokenAmount +): CurrencyAmount ``` Calculates the exact amount of token0 or token1 that the given amount of liquidity tokens represent. diff --git a/docs/sdk/v2/reference/02-token.md b/docs/sdk/v2/reference/02-token.md deleted file mode 100644 index 2594428cff..0000000000 --- a/docs/sdk/v2/reference/02-token.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -id: token -title: Token ---- - -```typescript -constructor(chainId: ChainId, address: string, decimals: number, symbol?: string, name?: string) -``` - -The Token entity represents an ERC-20 token at a specific address on a specific chain. - -# Example - -```typescript -import { ChainId, Token } from '@uniswap/sdk' - -const token = new Token(ChainId.MAINNET, '0xc0FFee0000000000000000000000000000000000', 18, 'HOT', 'Caffeine') -``` - -# Properties - -## chainId - -```typescript -chainId: ChainId -``` - -See [ChainId](other-exports/#chainid) - -## address - -```typescript -address: string -``` - -## decimals - -```typescript -decimals: number -``` - -## symbol - -```typescript -symbol?: string -``` - -## name - -```typescript -name?: string -``` - -# Methods - -## equals - -```typescript -equals(other: Token): boolean -``` - -Checks if the current instance is equal to another (has an identical chainId and address). - -## sortsBefore - -```typescript -sortsBefore(other: Token): boolean -``` - -Checks if the current instance sorts before another, by address. diff --git a/docs/sdk/v2/reference/03-route.md b/docs/sdk/v2/reference/03-route.md new file mode 100644 index 0000000000..0ed78f1698 --- /dev/null +++ b/docs/sdk/v2/reference/03-route.md @@ -0,0 +1,65 @@ +--- +id: route +title: Route +--- + +```typescript +constructor(pairs: Pair[], input: Token, output: Token) +``` + +The Route entity represents one or more ordered Uniswap pairs with a fully specified path from input token to output token. + +# Example + +```typescript +import { SupportedChainId, Token, CurrencyAmount } from '@uniswap/sdk-core' +import { Pair, Route } from '@uniswap/v2-sdk' + +const HOT = new Token(SupportedChainId.MAINNET, '0xc0FFee0000000000000000000000000000000000', 18, 'HOT', 'Caffeine') +const NOT = new Token(SupportedChainId.MAINNET, '0xDeCAf00000000000000000000000000000000000', 18, 'NOT', 'Caffeine') +const HOT_NOT = new Pair(CurrencyAmount.fromRawAmount(HOT, '2000000000000000000'), CurrencyAmount.fromRawAmount(NOT, '1000000000000000000')) + +const route = new Route([HOT_NOT], NOT, HOT) +``` + +# Properties + +## pairs + +```typescript +pairs: Pair[] +``` + +The ordered pairs that the route is comprised of. + +## path + +```typescript +path: Token[] +``` + +The full path from input token to output token. + +## input + +```typescript +input: Token +``` + +The input token. + +## output + +```typescript +output: Token +``` + +The output token. + +## midPrice + +```typescript +midPrice: Price +``` + +Returns the current mid price along the route. diff --git a/docs/sdk/v2/reference/04-route.md b/docs/sdk/v2/reference/04-route.md deleted file mode 100644 index 816d0a3956..0000000000 --- a/docs/sdk/v2/reference/04-route.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -id: route -title: Route ---- - -```typescript -constructor(pairs: Pair[], input: Token) -``` - -The Route entity represents one or more ordered Uniswap pairs with a fully specified path from input token to output token. - -# Example - -```typescript -import { ChainId, Token, TokenAmount, Pair, Route } from '@uniswap/sdk' - -const HOT = new Token(ChainId.MAINNET, '0xc0FFee0000000000000000000000000000000000', 18, 'HOT', 'Caffeine') -const NOT = new Token(ChainId.MAINNET, '0xDeCAf00000000000000000000000000000000000', 18, 'NOT', 'Caffeine') -const HOT_NOT = new Pair(new TokenAmount(HOT, '2000000000000000000'), new TokenAmount(NOT, '1000000000000000000')) - -const route = new Route([HOT_NOT], NOT) -``` - -# Properties - -## pairs - -```typescript -pairs: Pair[] -``` - -The ordered pairs that the route is comprised of. - -## path - -```typescript -path: Token[] -``` - -The full path from input token to output token. - -## input - -```typescript -input: string -``` - -The input token. - -## output - -```typescript -output: string -``` - -The output token. - -## midPrice - -```typescript -midPrice: Price -``` - -Returns the current mid price along the route. diff --git a/docs/sdk/v2/reference/05-trade.md b/docs/sdk/v2/reference/04-trade.md similarity index 52% rename from docs/sdk/v2/reference/05-trade.md rename to docs/sdk/v2/reference/04-trade.md index d60edbe5da..13373f7ce7 100644 --- a/docs/sdk/v2/reference/05-trade.md +++ b/docs/sdk/v2/reference/04-trade.md @@ -4,7 +4,7 @@ title: Trade --- ```typescript -constructor(route: Route, amount: TokenAmount, tradeType: TradeType) +constructor(route: Route, amount: CurrencyAmount, tradeType: TradeType) ``` The Trade entity represents a fully specified trade along a route. This entity supplies all the information necessary to craft a router transaction. @@ -12,14 +12,15 @@ The Trade entity represents a fully specified trade along a route. This entity s # Example ```typescript -import { ChainId, Token, TokenAmount, Pair, Trade, TradeType, Route } from '@uniswap/sdk' +import { SupportedChainId, Token, CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { Pair, Trade, Route } -const HOT = new Token(ChainId.MAINNET, '0xc0FFee0000000000000000000000000000000000', 18, 'HOT', 'Caffeine') -const NOT = new Token(ChainId.MAINNET, '0xDeCAf00000000000000000000000000000000000', 18, 'NOT', 'Caffeine') -const HOT_NOT = new Pair(new TokenAmount(HOT, '2000000000000000000'), new TokenAmount(NOT, '1000000000000000000')) -const NOT_TO_HOT = new Route([HOT_NOT], NOT) +const HOT = new Token(SupportedChainId.MAINNET, '0xc0FFee0000000000000000000000000000000000', 18, 'HOT', 'Caffeine') +const NOT = new Token(SupportedChainId.MAINNET, '0xDeCAf00000000000000000000000000000000000', 18, 'NOT', 'Caffeine') +const HOT_NOT = new Pair(CurrencyAmount.fromRawAmount(HOT, '2000000000000000000'), CurrencyAmount.fromRawAmount(NOT, '1000000000000000000')) +const NOT_TO_HOT = new Route([HOT_NOT], NOT, HOT) -const trade = new Trade(NOT_TO_HOT, new TokenAmount(NOT, '1000000000000000'), TradeType.EXACT_INPUT) +const trade = new Trade(NOT_TO_HOT, CurrencyAmount.fromRawAmount(NOT, '1000000000000000'), TradeType.EXACT_INPUT) ``` # Properties @@ -43,7 +44,7 @@ tradeType: TradeType ## inputAmount ```typescript -inputAmount: TokenAmount +inputAmount: CurrencyAmount ``` For exact input trades, this value should be passed as amountIn to router functions. For exact output trades, this value should be multiplied by a factor >1, representing slippage tolerance, and passed as amountInMax to router functions. @@ -51,7 +52,7 @@ For exact input trades, this value should be passed as amountIn to router functi ## outputAmount ```typescript -outputAmount: TokenAmount +outputAmount: CurrencyAmount ``` For exact output trades, this value should be passed as amountOut to router functions. For exact input trades, this value should be multiplied by a factor <1, representing slippage tolerance, and passed as amountOutMin to router functions. @@ -64,23 +65,13 @@ executionPrice: Price The average price that the trade would execute at. -## nextMidPrice +## priceImpact ```typescript -nextMidPrice: Price +priceImpact: Percent ``` -What the new mid price would be if the trade were to execute. - -## slippage - -```typescript -slippage: Percent -``` - -The slippage incurred by the trade. - -- Strictly > .30%. +The percent difference between the mid price before the trade and the trade execution price. # Methods @@ -89,7 +80,7 @@ In the context of the following two methods, slippage refers to the percent diff ## minimumAmountOut (since 2.0.4) ```typescript -minimumAmountOut(slippageTolerance: Percent): TokenAmount +minimumAmountOut(slippageTolerance: Percent): CurrencyAmount ``` Returns the minimum amount of the output token that should be received from a trade, given the slippage tolerance. @@ -99,13 +90,21 @@ Useful when constructing a transaction for a trade of type `EXACT_INPUT`. ## maximumAmountIn (since 2.0.4) ```typescript -maximumAmountIn(slippageTolerance: Percent): TokenAmount +maximumAmountIn(slippageTolerance: Percent): CurrencyAmount ``` Returns the maximum amount of the input token that should be spent on the trade, given the slippage tolerance. Useful when constructing a transaction for a trade of type `EXACT_OUTPUT`. +## worstExecutionPrice + +Return the execution price after accounting for slippage tolerance + +```typescript +worstExecutionPrice(slippageTolerance: Percent): Price +``` + # Static methods These static methods provide ways to construct ideal trades from lists of pairs. @@ -113,32 +112,48 @@ Note these methods do not perform any aggregation across routes, as routes are l It's possible that a better price can be had by combining multiple trades across different routes. +## exactIn + +Constructs an exact in trade with the given amount in and route. + +```typescript +Trade.exactIn(route: Route, amountIn: CurrencyAmount): Trade +``` + +## exactOut + +Constructs an exact out trade with the given amount out and route + +```typescript +Trade.exactOut(route: Route, amountOut: CurrencyAmount): Trade +``` + ## bestTradeExactIn -Given a list of pairs, a fixed amount in, and token amount out, -this method returns the best `maxNumResults` trades that swap -an input token amount to an output token, making at most `maxHops` hops. -The returned trades are sorted by output amount, in decreasing order, and -all share the given input amount. +Given a list of pairs, and a fixed amount in, returns the top `maxNumResults` trades that go from an input token +amount to an output token, making at most `maxHops` hops. +Note this does not consider aggregation, as routes are linear. It's possible a better route exists by splitting +the amount in among multiple routes. ```typescript Trade.bestTradeExactIn( pairs: Pair[], - amountIn: TokenAmount, - tokenOut: Token, + nextAmountIn: CurrencyAmount, + currencyOut: Token, { maxNumResults = 3, maxHops = 3 }: BestTradeOptions = {}): Trade[] ``` ## bestTradeExactOut -Similar to the above method, but targets a fixed output token amount. -The returned trades are sorted by input amount, in increasing order, -and all share the given output amount. +Similar to the above method but instead targets a fixed output amount given a list of pairs, +and a fixed amount out, returns the top `maxNumResults` trades that go from an input token to an output token amount, +making at most `maxHops` hops. Note this does not consider aggregation, as routes are linear. +It is possible a better route exists by splitting the amountIn among multiple routes. ```typescript Trade.bestTradeExactOut( pairs: Pair[], - tokenIn: Token, - amountOut: TokenAmount, + currencyIn: Token, + nextAmountOut: CurrencyAmount, { maxNumResults = 3, maxHops = 3 }: BestTradeOptions = {}): Trade[] ``` diff --git a/docs/sdk/v2/reference/05-other-exports.md b/docs/sdk/v2/reference/05-other-exports.md new file mode 100644 index 0000000000..65391dcb58 --- /dev/null +++ b/docs/sdk/v2/reference/05-other-exports.md @@ -0,0 +1,42 @@ +--- +id: other-exports +title: Other Exports +--- + +A enum denominating supported rounding options. + +# FACTORY_ADDRESS + +```typescript +import { FACTORY_ADDRESS } from '@uniswap/v2-sdk' +``` + +The [factory address](../../../contracts/v2/reference/smart-contracts/factory#address). + +# INIT_CODE_HASH + +```typescript +import { INIT_CODE_HASH } from '@uniswap/v2-sdk' +``` + +See [pair addresses](../../../contracts/v2/guides/smart-contract-integration/getting-pair-addresses). + +# MINIMUM_LIQUIDITY + +```typescript +import { MINIMUM_LIQUIDITY } from '@uniswap/v2-sdk' +``` + +See [minimum liquidity](../../../contracts/v2/reference/smart-contracts/pair#minimum-liquidity). + +# InsufficientReservesError + +```typescript +import { InsufficientReservesError } from '@uniswap/v2-sdk' +``` + +# InsufficientInputAmountError + +```typescript +import { InsufficientInputAmountError } from '@uniswap/v2-sdk' +``` diff --git a/docs/sdk/v2/reference/06-fractions.md b/docs/sdk/v2/reference/06-fractions.md deleted file mode 100644 index 26fafdca0b..0000000000 --- a/docs/sdk/v2/reference/06-fractions.md +++ /dev/null @@ -1,276 +0,0 @@ ---- -id: fractions -title: Fractions ---- - -# Fraction - -```typescript -constructor(numerator: BigintIsh, denominator: BigintIsh = ONE) -``` - -The base class which all subsequent fraction classes extend. **Not meant to be used directly.** - -## Properties - -### numerator - -```typescript -numerator: JSBI -``` - -### denominator - -```typescript -denominator: JSBI -``` - -### quotient - -```typescript -quotient: JSBI -``` - -Performs floor division. - -## Methods - -### invert - -```typescript -invert(): Fraction -``` - -### add - -```typescript -add(other: Fraction | BigintIsh): Fraction -``` - -### subtract - -```typescript -subtract(other: Fraction | BigintIsh): Fraction -``` - -### multiply - -```typescript -multiply(other: Fraction | BigintIsh): Fraction -``` - -### divide - -```typescript -divide(other: Fraction | BigintIsh): Fraction -``` - -### toSignificant - -```typescript -toSignificant( - significantDigits: number, - format: object = { groupSeparator: '' }, - rounding: Rounding = Rounding.ROUND_HALF_UP -): string -``` - -Formats a fraction to the specified number of significant digits. - -- For format options, see [toFormat](https://github.com/MikeMcl/toFormat). - -### toFixed - -```typescript -toFixed( - decimalPlaces: number, - format: object = { groupSeparator: '' }, - rounding: Rounding = Rounding.ROUND_HALF_UP -): string -``` - -Formats a fraction to the specified number of decimal places. - -- For format options, see [toFormat](https://github.com/MikeMcl/toFormat). - -# Percent - -Responsible for formatting percentages (10% instead of 0.1). - -## Example - -```typescript -import { Percent } from '@uniswap/sdk' - -const percent = new Percent('60', '100') -console.log(percent.toSignificant(2)) // 60 -``` - -### toSignificant - -See [toSignificant](#tosignificant). - -### toFixed - -See [toFixed](#tofixed). - -# TokenAmount - -```typescript -constructor(token: Token, amount: BigintIsh) -``` - -Responsible for formatting token amounts with specific decimal places. - -## Example - -```typescript -import { Token, TokenAmount } from '@uniswap/sdk' - -const FRIED = new Token(ChainId.MAINNET, '0xfa1aFe1000000000000000000000000000000000', 18, 'FRIED', 'Beans') - -const tokenAmount = new TokenAmount(FRIED, '3000000000000000000') -console.log(tokenAmount.toExact()) // 3 -``` - -## Properties - -### token - -```typescript -token: Token -``` - -### raw - -```typescript -raw: JSBI -``` - -Returns the full token amount, unadjusted for decimals. - -## Methods - -### add - -```typescript -add(other: TokenAmount): TokenAmount -``` - -### subtract - -```typescript -subtract(other: TokenAmount): TokenAmount -``` - -### toSignificant - -See [toSignificant](#tosignificant). - -### toFixed - -See [toFixed](#tofixed). - -### toExact - -```typescript -toExact(format: object = { groupSeparator: '' }): string -``` - -# Price - -```typescript -constructor(baseToken: Token, quoteToken: Token, denominator: BigintIsh, numerator: BigintIsh) -``` - -Responsible for denominating the relative price between two tokens. Denominator and numerator must be unadjusted for decimals. - -## Example - -```typescript -import { ChainId, WETH as WETHs, Token, Price } from '@uniswap/sdk' - -const WETH = WETHs[ChainId.MAINNET] -const ABC = new Token(ChainId.MAINNET, '0xabc0000000000000000000000000000000000000', 18, 'ABC') - -const price = new Price(WETH, ABC, '1000000000000000000', '123000000000000000000') -console.log(price.toSignificant(3)) // 123 -``` - -This example shows the ETH/XYZ price, where ETH is the base token, and XYZ is the quote token. The price is constructed from an amount of XYZ (the numerator) / an amount of WETH (the denominator). - -## Static Methods - -### fromRoute - -```typescript -fromRoute(route: Route): Price -``` - -## Properties - -### baseToken - -```typescript -baseToken: Token -``` - -### quoteToken - -```typescript -quoteToken: Token -``` - -### scalar - -```typescript -scalar: Fraction -``` - -Used to adjust the price for the decimals of the base and quote tokens. - -### raw - -```typescript -raw: Fraction -``` - -Returns the raw price, unadjusted for decimals. - -### adjusted - -```typescript -adjusted: Fraction -``` - -Returns the price, adjusted for decimals. - -## Methods - -### invert - -```typescript -invert(): Price -``` - -### multiply - -```typescript -multiply(other: Price): Price -``` - -### quote - -```typescript -quote(tokenAmount: TokenAmount): TokenAmount -``` - -Given an asset amount, returns an equivalent value of the other asset, according to the current price. - -### toSignificant - -See [toSignificant](#tosignificant). - -### toFixed - -See [toFixed](#tofixed). diff --git a/docs/sdk/v2/reference/07-fetcher.md b/docs/sdk/v2/reference/07-fetcher.md deleted file mode 100644 index 9bc279b9d2..0000000000 --- a/docs/sdk/v2/reference/07-fetcher.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -id: fetcher -title: Fetcher ---- - -The data fetching logic is split from the rest of the code for better tree-shaking, -i.e. so that it does not get packaged into your code unless it is used. -The SDK is otherwise unconcerned with how you get data from the blockchain. - -This class contains static methods for constructing instances of pairs and tokens -from on-chain data. It cannot be constructed. - -# Static Methods - -## fetchTokenData - -```typescript -async fetchTokenData( - chainId: ChainId, - address: string, - provider = getDefaultProvider(getNetwork(chainId)), - symbol?: string, - name?: string -): Promise -``` - -Initializes a class instance from a chainId and token address, if the decimals of the token are unknown and cannot be fetched externally. Decimals are fetched via an [ethers.js](https://github.com/ethers-io/ethers.js/) v5 provider. If not passed in, a default provider is used. - -## fetchPairData - -```typescript -async fetchPairData( - tokenA: Token, - tokenB: Token, - provider = getDefaultProvider(getNetwork(tokenA.chainId)) -): Promise -``` - -Initializes a class instance from two Tokens, if the pair's balances of these tokens are unknown and cannot be fetched externally. Pair reserves are fetched via an [ethers.js](https://github.com/ethers-io/ethers.js/) v5 provider. If not passed in, a default provider is used. diff --git a/docs/sdk/v2/reference/08-other-exports.md b/docs/sdk/v2/reference/08-other-exports.md deleted file mode 100644 index e4b7c322c2..0000000000 --- a/docs/sdk/v2/reference/08-other-exports.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -id: other-exports -title: Other Exports ---- - -# JSBI - -```typescript -import { JSBI } from '@uniswap/sdk' -// import JSBI from 'jsbi' -``` - -The default export from [jsbi](https://github.com/GoogleChromeLabs/jsbi). - -# BigintIsh - -```typescript -import { BigintIsh } from '@uniswap/sdk' -// type BigintIsh = JSBI | bigint | string -``` - -A union type comprised of all types that can be cast to a JSBI instance. - -# ChainId - -```typescript -import { ChainId } from '@uniswap/sdk' -// enum ChainId { -// MAINNET = 1, -// ROPSTEN = 3, -// RINKEBY = 4, -// GÖRLI = 5, -// KOVAN = 42 -// } -``` - -A enum denominating supported chain IDs. - -# TradeType - -```typescript -import { TradeType } from '@uniswap/sdk' -// enum TradeType { -// EXACT_INPUT, -// EXACT_OUTPUT -// } -``` - -A enum denominating supported trade types. - -# Rounding - -```typescript -import { Rounding } from '@uniswap/sdk' -// enum Rounding { -// ROUND_DOWN, -// ROUND_HALF_UP, -// ROUND_UP -// } -``` - -A enum denominating supported rounding options. - -# FACTORY_ADDRESS - -```typescript -import { FACTORY_ADDRESS } from '@uniswap/sdk' -``` - -The [factory address](../../../contracts/v2/reference/smart-contracts/factory#address). - -# INIT_CODE_HASH - -```typescript -import { INIT_CODE_HASH } from '@uniswap/sdk' -``` - -See [pair addresses](../../../contracts/v2/guides/smart-contract-integration/getting-pair-addresses). - -# MINIMUM_LIQUIDITY - -```typescript -import { MINIMUM_LIQUIDITY } from '@uniswap/sdk' -``` - -See [minimum liquidity](../../../contracts/v2/reference/smart-contracts/pair#minimum-liquidity). - -# InsufficientReservesError - -```typescript -import { InsufficientReservesError } from '@uniswap/sdk' -``` - -# InsufficientInputAmountError - -```typescript -import { InsufficientInputAmountError } from '@uniswap/sdk' -``` - -# WETH - -```typescript -import { WETH } from '@uniswap/sdk' -``` - -An object whose values are [WETH](../../../contracts/v2/reference/smart-contracts/router-02#weth) [Token](token) instances, indexed by [ChainId](#chainid). From 93abad164112765310886b4a4a15b28a65c713b8 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Wed, 31 May 2023 18:53:18 +0100 Subject: [PATCH 07/52] Fix: Link to Token in V2 Fetching Data guide --- docs/sdk/v2/guides/02-fetching-data.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sdk/v2/guides/02-fetching-data.md b/docs/sdk/v2/guides/02-fetching-data.md index 492f5938c5..4cf1f7497d 100644 --- a/docs/sdk/v2/guides/02-fetching-data.md +++ b/docs/sdk/v2/guides/02-fetching-data.md @@ -26,7 +26,7 @@ The next piece of data we need is **decimals**. ### Provided by the User -One option here is to simply pass in the correct value, which we may know is `18`. At this point, we're ready to represent DAI as a [Token](../reference/token): +One option here is to simply pass in the correct value, which we may know is `18`. At this point, we're ready to represent DAI as a [Token](../../core/reference/classes/Token.md): ```typescript import { SupportedChainId, Token } from '@uniswap/sdk-core' From d8dc549fc1a14b2759c63ce2605b8029974b4a5d Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Sun, 4 Jun 2023 12:50:10 +0100 Subject: [PATCH 08/52] Add pool data guide --- docs/sdk/v3/guides/05-pool-data.md | 131 +++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 docs/sdk/v3/guides/05-pool-data.md diff --git a/docs/sdk/v3/guides/05-pool-data.md b/docs/sdk/v3/guides/05-pool-data.md new file mode 100644 index 0000000000..7d6e16d69a --- /dev/null +++ b/docs/sdk/v3/guides/05-pool-data.md @@ -0,0 +1,131 @@ +--- +id: pool-data +title: Fetching Pool Data +--- + +## Introduction + +This guide will cover how to initialize a Pool with full tick data to allow offchain calculations. It is based on the [Fetching Pool data example](https://github.com/Uniswap/examples/tree/main/v3-sdk/pool-data), found in the Uniswap code examples [repository](https://github.com/Uniswap/examples). To run this example, check out the guide's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/pool-data/README.md) and follow the setup instructions. + +:::info +If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](./01-background.md) page! +::: + +In this example we will use **ethers JS** and **The Graph** to construct a `Pool` object that we can use in the following guides. + +This guide will **cover**: + +1. Computing the Pool's address +2. Referencing the Pool contract and fetching metadata +3. Using the V3 subgraph to fetch Tick data + +At the end of the guide, we should be able to view the full tick data of a pool using the example's included UI. + +For this guide, the following Uniswap packages are used: + +- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) +- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) + +The core code of this guide can be found in [`pool.ts`](https://github.com/Uniswap/v3-sdk/blob/main/src/entities/pool.ts) + +## Computing the Pool's deployment address + +In this example, we will construct the **USDC - WETH** Pool with **LOW** fees. The SDK provides a method to compute the address: + +```typescript +import { Pool, FeeAmount } from '@uniswap/v3-sdk' +import { Token, WETH9 } from '@uniswap/sdk-core' + +const USDC = new Token(1, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 6) +const WETH = WETH9[USDC.chainId] +const poolAddress = Pool.getAddress( + tokenA, + tokenB, + FeeAmount.LOW + ) +``` + +Uniswap V3 allows different Fee tiers when deploying a pool, so multiple pools can exist for each pair of tokens. + +## Creating a Pool Contract instance and fetching metadata + +Now that we have the address of a **USDC - ETH** Pool, we can construct an instance of an **ethers** `Contract` to interact with it. +To construct the Contract we need to provide the address of the contract, its ABI and the provider that will carry out the RPC call for us. We get access to the contract's ABI through the @uniswap/v3-core package, which holds the core smart contracts of the Uniswap V3 protocol: + +```typescript +import { ethers } from 'ethers +import IUniswapV3PoolABI from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json' + +const poolContract = new ethers.Contract( + poolAddress, + IUniswapV3PoolABI.abi, + provider +) +``` + +Once we have set up our reference to the contract, we can proceed to access its methods. To construct our offchain representation of the Pool Contract, we need to fetch its liquidity, sqrtPrice, currently active tick and the full Tick data. +We get the **liquidity**, **sqrtPrice** and **tick** directly from the blockchain: + +```typescript +const [liquidity, slot0] = + await Promise.all([ + poolContract.liquidity(), + poolContract.slot0(), + ]) +``` + +## Fetching all Ticks + +V3 pools use ticks to [concentrate liquidity](../../../concepts/protocol/concentrated-liquidity.md) in price ranges and allow for better pricing of trades. +Even though most Pools only have a couple of initialized ticks, it is possible that a pools liquidity is spread among thousands of ticks. +In that case, it can be very expensive or slow to get all of them with RPC calls. + +To fetch all ticks of the **USDC - WETH Pool**, we will use the [Uniswap V3 graph](../../../api/subgraph/overview.md). To construct a `Tick` for the SDK, we need the **tickIdx**, the **liquidityGross** and the **liquidityNet**. +We define our GraphQL query: + +```{ + ticks ( + where: { + poolAddress: "${poolAddress.toLowerCase()}", + liquidityGross_gt: "0"} + first: 1000 + ) { + tickIdx + liquidityGross + liquidityNet + } +} +``` + +We only fetch the ticks that have liquidity, and we convert the poolAddress to lower case for the subgraph to work with. +To create our Pool, we need to map the raw data we received from the subgraph to `Tick` objects that the SDK can work with: + +```typescript +const sdkTicks = ticks.map((graphTick: GraphTick) => { + return new Tick({ + index: +graphTick.tickIdx, + liquidityGross: graphTick.liquidityGross, + liquidityNet: graphTick.liquidityNet + }) + }) +``` + +:::note +GraphQL is only able to fetch 1000 records at a time. If a pool has more than 1000 initialized ticks, multiple calls are necessary to get all of them. +::: + +We have everything to construct our `Pool` now: + +```typescript +const usdcWethPool = new Pool( + tokenA, + tokenB, + feeAmount, + slot0.sqrtPriceX96, + liquidity, + slot0.tick, + sdkTicks +) +``` + +With this fully initialized Pool, we can calculate swaps on it offchain, without the need to make expensive RPC calls. From 10040db2bbd34ec097006e94cbdcfa1fbaad1b7c Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Sun, 4 Jun 2023 14:05:08 +0100 Subject: [PATCH 09/52] Move pool data to advanced, update guide and add Overview --- docs/sdk/v3/guides/advanced/01-overview.md | 16 +++++ .../02-pool-data.md} | 60 +++++++++++++------ docs/sdk/v3/guides/advanced/_category_.json | 5 ++ 3 files changed, 64 insertions(+), 17 deletions(-) create mode 100644 docs/sdk/v3/guides/advanced/01-overview.md rename docs/sdk/v3/guides/{05-pool-data.md => advanced/02-pool-data.md} (69%) create mode 100644 docs/sdk/v3/guides/advanced/_category_.json diff --git a/docs/sdk/v3/guides/advanced/01-overview.md b/docs/sdk/v3/guides/advanced/01-overview.md new file mode 100644 index 0000000000..d4cc556dda --- /dev/null +++ b/docs/sdk/v3/guides/advanced/01-overview.md @@ -0,0 +1,16 @@ +--- +id: overview +title: Overview +--- + +For some more advanced use cases, it is necessary to use multiple tools in the Uniswap toolchain. + +:::info +If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](../01-background.md) page! +::: + +The following examples use **ethersJS** and the **Uniswap V3 subgraph** hosted on The Graph's hosted service. To learn more about Uniswap's subgraphs, visit the [API](../../../../api/subgraph/overview.md) section. + +We will take a deep dive into the Uniswap V3 protocol and use practical examples to understand the data stored by the Uniswap smart contracts. We will explore how we can compute the available liquidity in a specific price range, visualize **liquidity density** in pools and how to use Uniswap as a **price oracle**. + +These guides are a bit longer than the previous ones and provide more complete code examples. diff --git a/docs/sdk/v3/guides/05-pool-data.md b/docs/sdk/v3/guides/advanced/02-pool-data.md similarity index 69% rename from docs/sdk/v3/guides/05-pool-data.md rename to docs/sdk/v3/guides/advanced/02-pool-data.md index 7d6e16d69a..2f7d72a773 100644 --- a/docs/sdk/v3/guides/05-pool-data.md +++ b/docs/sdk/v3/guides/advanced/02-pool-data.md @@ -11,7 +11,7 @@ This guide will cover how to initialize a Pool with full tick data to allow offc If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](./01-background.md) page! ::: -In this example we will use **ethers JS** and **The Graph** to construct a `Pool` object that we can use in the following guides. +In this example we will use **ethers JS** and **The Graph** to construct a `Pool` object that we can use in the following guides. We will also [Axios](https://axios-http.com/docs/intro) for requests, but any http client is fine. This guide will **cover**: @@ -50,12 +50,13 @@ Uniswap V3 allows different Fee tiers when deploying a pool, so multiple pools c ## Creating a Pool Contract instance and fetching metadata Now that we have the address of a **USDC - ETH** Pool, we can construct an instance of an **ethers** `Contract` to interact with it. -To construct the Contract we need to provide the address of the contract, its ABI and the provider that will carry out the RPC call for us. We get access to the contract's ABI through the @uniswap/v3-core package, which holds the core smart contracts of the Uniswap V3 protocol: +To construct the Contract we need to provide the address of the contract, its ABI and a provider connected to an [RPC endpoint](https://docs.infura.io/infura/getting-started). We get access to the contract's ABI through the `@uniswap/v3-core` package, which holds the core smart contracts of the Uniswap V3 protocol: ```typescript import { ethers } from 'ethers import IUniswapV3PoolABI from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json' +const provider = new ethers.provider.JsonRpcProvider(rpcUrl) const poolContract = new ethers.Contract( poolAddress, IUniswapV3PoolABI.abi, @@ -64,7 +65,7 @@ const poolContract = new ethers.Contract( ``` Once we have set up our reference to the contract, we can proceed to access its methods. To construct our offchain representation of the Pool Contract, we need to fetch its liquidity, sqrtPrice, currently active tick and the full Tick data. -We get the **liquidity**, **sqrtPrice** and **tick** directly from the blockchain: +We get the **liquidity**, **sqrtPrice** and **tick** directly from the blockchain by calling `liquidity()`and `slot0()` on the Pool contract: ```typescript const [liquidity, slot0] = @@ -74,6 +75,16 @@ const [liquidity, slot0] = ]) ``` +The [slot0 function](../../../../contracts/v3/reference/core/interfaces/pool/IUniswapV3PoolState.md#slot0) represents the first (0th) storage slot of the pool and exposes multiple useful values in a single function: + +```solidity + function slot0( + ) external view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked) +``` + +For our use case, we only need the `sqrtPriceX96` and the currently active `tick`. + + ## Fetching all Ticks V3 pools use ticks to [concentrate liquidity](../../../concepts/protocol/concentrated-liquidity.md) in price ranges and allow for better pricing of trades. @@ -81,23 +92,34 @@ Even though most Pools only have a couple of initialized ticks, it is possible t In that case, it can be very expensive or slow to get all of them with RPC calls. To fetch all ticks of the **USDC - WETH Pool**, we will use the [Uniswap V3 graph](../../../api/subgraph/overview.md). To construct a `Tick` for the SDK, we need the **tickIdx**, the **liquidityGross** and the **liquidityNet**. -We define our GraphQL query: - -```{ - ticks ( - where: { - poolAddress: "${poolAddress.toLowerCase()}", - liquidityGross_gt: "0"} - first: 1000 - ) { - tickIdx - liquidityGross - liquidityNet +We define our GraphQL query and [send a POST request](https://axios-http.com/docs/post_example) to the V3 subgraph API endpoint: + +```typescript +axios.post( + "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3", + {"query": `{ ticks( + where: {poolAddress: "${poolAddress.toLowerCase()}", liquidityNet_not: "0"} + first: 1000, + skip: ${skip}, + orderBy: tickIdx, + orderDirection: asc + ) { + tickIdx + liquidityGross + liquidityNet + } + }` + }, + { + headers: { + "Content-Type": "application/json" + } } -} + ) ``` -We only fetch the ticks that have liquidity, and we convert the poolAddress to lower case for the subgraph to work with. +We only fetch the ticks that have liquidity, and we convert the poolAddress to lower case for the subgraph to work with. To make sure the Ticks are ordered correctly, we also define the order direction in the query. + To create our Pool, we need to map the raw data we received from the subgraph to `Tick` objects that the SDK can work with: ```typescript @@ -129,3 +151,7 @@ const usdcWethPool = new Pool( ``` With this fully initialized Pool, we can calculate swaps on it offchain, without the need to make expensive RPC calls. + +## Next Steps + +Now that you are familiar with using TheGraph, continue your journey with the next example on visualizing Liquidity density. diff --git a/docs/sdk/v3/guides/advanced/_category_.json b/docs/sdk/v3/guides/advanced/_category_.json new file mode 100644 index 0000000000..65c1e31617 --- /dev/null +++ b/docs/sdk/v3/guides/advanced/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Advanced", + "position": 6, + "collapsed": true +} From b55b45c8091a613bf505c5b0923d05885b7049f2 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Mon, 5 Jun 2023 17:30:55 +0100 Subject: [PATCH 10/52] Add Active Liquidity Guide --- docs/sdk/v3/guides/advanced/01-overview.md | 2 +- docs/sdk/v3/guides/advanced/02-pool-data.md | 5 +- .../v3/guides/advanced/03-active-liquidity.md | 229 ++++++++++++++++++ .../images/liquidityNetComparison.png | Bin 0 -> 99461 bytes 4 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 docs/sdk/v3/guides/advanced/03-active-liquidity.md create mode 100644 docs/sdk/v3/guides/advanced/images/liquidityNetComparison.png diff --git a/docs/sdk/v3/guides/advanced/01-overview.md b/docs/sdk/v3/guides/advanced/01-overview.md index d4cc556dda..f02ec5c5d1 100644 --- a/docs/sdk/v3/guides/advanced/01-overview.md +++ b/docs/sdk/v3/guides/advanced/01-overview.md @@ -13,4 +13,4 @@ The following examples use **ethersJS** and the **Uniswap V3 subgraph** hosted o We will take a deep dive into the Uniswap V3 protocol and use practical examples to understand the data stored by the Uniswap smart contracts. We will explore how we can compute the available liquidity in a specific price range, visualize **liquidity density** in pools and how to use Uniswap as a **price oracle**. -These guides are a bit longer than the previous ones and provide more complete code examples. +These guides are a bit longer than the previous ones and provide more theoratical background and complete code examples. diff --git a/docs/sdk/v3/guides/advanced/02-pool-data.md b/docs/sdk/v3/guides/advanced/02-pool-data.md index 2f7d72a773..1b454a9991 100644 --- a/docs/sdk/v3/guides/advanced/02-pool-data.md +++ b/docs/sdk/v3/guides/advanced/02-pool-data.md @@ -18,8 +18,9 @@ This guide will **cover**: 1. Computing the Pool's address 2. Referencing the Pool contract and fetching metadata 3. Using the V3 subgraph to fetch Tick data +4. Constructing the Pool object -At the end of the guide, we should be able to view the full tick data of a pool using the example's included UI. +At the end of the guide, we will have created a `Pool` Object that accurately represents the state of a V3 pool at the time we fetched it. For this guide, the following Uniswap packages are used: @@ -154,4 +155,4 @@ With this fully initialized Pool, we can calculate swaps on it offchain, without ## Next Steps -Now that you are familiar with using TheGraph, continue your journey with the next example on visualizing Liquidity density. +Now that you are familiar with using TheGraph, continue your journey with the next example on visualizing the Liquidity density of a pool. diff --git a/docs/sdk/v3/guides/advanced/03-active-liquidity.md b/docs/sdk/v3/guides/advanced/03-active-liquidity.md new file mode 100644 index 0000000000..4853cce9ea --- /dev/null +++ b/docs/sdk/v3/guides/advanced/03-active-liquidity.md @@ -0,0 +1,229 @@ +--- +id: active-liquidity +title: Active Liquidity +--- + +## Introduction + +This guide will cover how to fetch and compute the active liquidity in the specific tick ranges of a pool. It is based on the [Liquidity Density example](https://github.com/Uniswap/examples/tree/main/v3-sdk/liquidity-density) and can be seen used in production, albeit in a more sophisticated way, in the [Uniswap Analytics](https://info.uniswap.org/#/pools) website. + +:::info +If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](./01-background.md) page! +::: + +In this guide, we will use the Tick data we fetched from the V3 subgraph in the previous guide and compute the active liquidity our Pool can use at each tick. We then use `recharts` to draw a chart that visualizes our Pool's liqudity density. + +This guide will cover: + +1. Getting the tickSpacing and currently active Tick from the Pool +2. Calculating active liquidity from net liquidity +3. Drawing a chart from the tick data + +This guide will not cover: + +- Specifics of working with the recharts library. You can read more about that [here](https://recharts.org/en-US/). + +At the end of the guide, we should be able to visualize the liquidity of any V3 Pool. + +## Understanding Active Liquidity + +To visualize the distribution of active liquidity in our Pool, we want to draw our Chart around the currently active Tick. For that we have to first understand: + +- What is an initialized Tick? +- What is the current Tick? + +### Initialized Ticks + +When providing liquidity for a pool, the LP decides the **price range** in which the liquidity should be provided, and the amount of liquidity to be provided. +The pool understands the position as **liquidity between the lower and upper tick**. The Tick Index in this context is a representation of the price between the Pool's assets. +Looking at this [visualization](https://www.desmos.com/calculator/oduetjzfp4) of multiple positions in a V3 Pool, we can see that the liquidity available for a swap does not change inside a position, but when crossing into the next position. +This is what the **Initialized Ticks** of a Pool represent - they are a representation of the start or end of one or more positions. + +LiquidityNet1 + +When entering or leaving a position, its liquidity is added or correspondingly removed from the active liquidity available for a Swap. The initialized Ticks store this change in available liquidity in the `liquidityNet` field. +The change is always stored in relation to the currently active tick - the current price. When the price crosses an initialized Tick, it gets updated and liqudity that was previously added when crossing the Tick would now be removed and vice versa. + +We already fetched the initialized ticks of our Pool in the [previous guide](./02-pool-data.md). The format we got from the Graph is: + +```typescript +interface GraphTick { + tickIdx: string + liquidityGross: string + liquidityNet: string +} +``` + +### Current Tick + +The current Tick of the Pool represents the price after the last swap. Considering that the initialized Ticks only represent positions, we see that it is not necessarily one of the initialized Ticks but can be at any point in between them. +The active liqudity at the current Price is also stored in the smart contract - we already fetched it with the slot0 function. + +### Tickspacing + +Only the ticks with indices that are dividable by the tickspacing of a Pool are initializable. The Tickspacing of the Pool is dependent on the Fee Tier. Pools with lower fees are meant to be used for more stable Token Pairs and allow for more granularity in where LPs position their liquidity. We can get the `tickSpacing`from our pool: + +```typescript +const tickSpacing = pool.tickSpacing +``` + +### Putting it all together + +For the purpose of visualizing the liquidity density of the Pool, it rarely makes sense to display the full Tick Range of the Pool, as the vast majority of liquidity will be focused in a narrow price range. + +Instead, we will display a sensible number of ticks around the current price. + +## Calculating active liquidity + +We know the spacing between ticks and the Initialized Ticks where active liquidity changes. All we have to do is start calculating from the current Tick and iterate outwards. +To draw our chart we want a data structure that looks something like this: + +```typescript +import { Price } from '@uniswap/sdk-core' + +interface TickProcessed { + tickIdx: number, + liquidityActive: JSBI, + liquidityNet: JSBI, + price0: Price, + price1: Price, + isCurrent: boolean +} +``` + +To access the initialized Ticks directly from their Tick Index, we store them in a [Record](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type): + +```typescript +const tickIdxToTickDictionary: Record = Object.fromEntries( + ticks.map((graphTick) => [graphTick.tickIdx, graphTick]) + ) +``` + +The ticks variable in this code snippet is the response we got from the V3 Subgraph. + +We want to mark the Tick closest to the current Price and we want to be able to display the prices at a Tick to the user. +We calculate the **initializable Tick** closest to the current price and create the active Tick that we start from: + +```typescript +import { tickToPrice } from '@uniswap/v3-sdk' + +const activeTickIdx = Math.floor(pool.tickCurrent / tickSpacing) * tickSpacing + +const activeTickProcessed: TickProcessed = { + tickIdx: activeTickIdx, + liquidityActive: pool.liquidity, + liquidityNet: JSBI.BigInt(0), + price0: tickToPrice(tokenA, tokenB, activeTickIdx), + price1: tickToPrice(tokenB, tokenA, activeTickIdx), + isCurrent: true +} +``` + +Here we also calculate the price of the tokens from the tickIdx, the `v3-sdk` exports a handy utility function for that. +If the active Tick is initialized, we also need to set the liquidityNet to correctly handle moving out of the position: + +```typescript +const currentTickInitialized = tickIdxToTickDictionary[activeTickIdx] +if (currentTickInitialized !== undefined) { + activeTickProcessed.liquidityNet = JSBI.BigInt(currentTickInitialized.liquidityNet) +} +``` + +We now start iterating outwards from the active Tick and compute the active liquidity for each Tick we want to display. The processed Tick is then saved in an Array of `TickProcessed`. +We choose an arbitrary number of ticks we want to display, for this example we calculate 100 Ticks in each direction. + +```typescript +import { TickMath } from '@uniswap/v3-sdk' + +let previousTickProcessed = { + ...activeTickProcessed +} + +processedTicks: TickProcessed[] = [] + +for (let i = 0; i < 100; i++) { + const currentTickIdx = previousTickProcessed.tickIdx + tickSpacing + + if (currentTickIdx > TickMath.MAX_TICK) { + break + } + + const currentTickProcessed = { + liquidityActive: previousTickProcessed.liquidityActive, + tickIdx: currentTickIdx, + liquidityNet: JSBI.BigInt(0), + price0: tickToPrice(token0, token1, currentTickIdx), + price1: tickToPrice(token1, token0, currentTickIdx), + isActive: false + } + + ... +} +``` + +We calculate one Tick at a time, and we need to make sure our Tick stays inside the possible price range. +Again, we check if our current Tick is initialized and if so, recalculate the active liquidity: + +```typescript +for (let i = 0; i < 100; i++) { + + ... + + const currentTickInitialized = tickIdxToTickDictionary[currentTickIdx] + + if (currentTickInitialized !== undefined) { + currentTickProcessed.liquidityNet = JSBI.BigInt(currentTickInitialized.liquidityNet) + currentTickProcessed.liquidityActive = JSBI.add( + previousTickProcessed.liquidityActive, + JSBI.BigInt(currentTickInitialized.liquidityNet) + ) + } + + processedTicks.push(currentTickProcessed) + previousTickProcessed = currentTickProcessed +} +``` + +After we are done calculating the next 100 Ticks after the current Tick, we iterate in the opposite direction for the previous Ticks. Iterating downwards, we need to subtract the net liquidity where we added it when iterating upwards. +You can find a full code example in the [Uniswap Example repository](https://github.com/Uniswap/examples/tree/main/v3-sdk/active-liquidity). +We are finally able to combine the previous, active and subsequent ticks: + +```typescript +const allProcessedTicks = previousTicks.concat(activeTickProcessed).concat(subsequentTicks) +``` + +## Drawing the Chart + +We are done with our calculations and move on to displaying the data. **Recharts** is not able to handle JSBI, so we need to convert the Array we created to a format it can handle: + +```typescript +const chartTicks: TicksChart[] = allProcessedTicks.map((tickProcessed) => { + return {...processedTick, liquidityActiveChart: parseFloat(tickProcessed.liquidityActive.toString())} +}) +``` + +The loss of precision will not be visually noticeable in the chart and we are still able to display the exact number in a Tooltip if we wish to. +Finally, we draw the Chart: + +```jsx + + + + + + {chartTicks.map((entry, index) => { + return ( + + ) + })} + + + +``` + +In a real application, you will probably want to format the chart properly and display additional information for users. +Check out the full [code example](https://github.com/Uniswap/examples/tree/main/v3-sdk/active-liquidity) to this guide and the official recharts [documentation](https://recharts.org/). +You can also take a look at the [Uniswap Info](https://github.com/Uniswap/v3-info) repository to see a similar chart used in production. diff --git a/docs/sdk/v3/guides/advanced/images/liquidityNetComparison.png b/docs/sdk/v3/guides/advanced/images/liquidityNetComparison.png new file mode 100644 index 0000000000000000000000000000000000000000..1c480ffda27e7405c5853f8879d391efe16cb472 GIT binary patch literal 99461 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

f8y!uTP`&1dc7g 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 literal 0 HcmV?d00001 From 20108855506beb1dc041a1d1ca5619a6f2deed3c Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Mon, 5 Jun 2023 17:38:55 +0100 Subject: [PATCH 11/52] Add Next step to active liquidity guide --- docs/sdk/v3/guides/advanced/03-active-liquidity.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/sdk/v3/guides/advanced/03-active-liquidity.md b/docs/sdk/v3/guides/advanced/03-active-liquidity.md index 4853cce9ea..73d8125fc4 100644 --- a/docs/sdk/v3/guides/advanced/03-active-liquidity.md +++ b/docs/sdk/v3/guides/advanced/03-active-liquidity.md @@ -226,4 +226,9 @@ Finally, we draw the Chart: In a real application, you will probably want to format the chart properly and display additional information for users. Check out the full [code example](https://github.com/Uniswap/examples/tree/main/v3-sdk/active-liquidity) to this guide and the official recharts [documentation](https://recharts.org/). + You can also take a look at the [Uniswap Info](https://github.com/Uniswap/v3-info) repository to see a similar chart used in production. + +## Next Steps + +Now that you are familiar with liquidity data, consider checking out our next guide on using Uniswap as a Price Oracle. \ No newline at end of file From fd9a35a2903cc54c011be25b2ad4fa9d59bec241 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Wed, 7 Jun 2023 12:31:57 +0100 Subject: [PATCH 12/52] Add price oracle file, change price types to string --- docs/sdk/v3/guides/advanced/03-active-liquidity.md | 12 ++++++------ docs/sdk/v3/guides/advanced/04-price-oracle.md | 4 ++++ 2 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 docs/sdk/v3/guides/advanced/04-price-oracle.md diff --git a/docs/sdk/v3/guides/advanced/03-active-liquidity.md b/docs/sdk/v3/guides/advanced/03-active-liquidity.md index 73d8125fc4..4d122b3f58 100644 --- a/docs/sdk/v3/guides/advanced/03-active-liquidity.md +++ b/docs/sdk/v3/guides/advanced/03-active-liquidity.md @@ -79,14 +79,13 @@ We know the spacing between ticks and the Initialized Ticks where active liquidi To draw our chart we want a data structure that looks something like this: ```typescript -import { Price } from '@uniswap/sdk-core' interface TickProcessed { tickIdx: number, liquidityActive: JSBI, liquidityNet: JSBI, - price0: Price, - price1: Price, + price0: string, + price1: string, isCurrent: boolean } ``` @@ -113,13 +112,14 @@ const activeTickProcessed: TickProcessed = { tickIdx: activeTickIdx, liquidityActive: pool.liquidity, liquidityNet: JSBI.BigInt(0), - price0: tickToPrice(tokenA, tokenB, activeTickIdx), - price1: tickToPrice(tokenB, tokenA, activeTickIdx), + price0: tickToPrice(tokenA, tokenB, activeTickIdx).toFixed(6), + price1: tickToPrice(tokenB, tokenA, activeTickIdx).toFixed(6), isCurrent: true } ``` -Here we also calculate the price of the tokens from the tickIdx, the `v3-sdk` exports a handy utility function for that. +Here we also calculate the price of the tokens from the tickIdx, the `v3-sdk` exports a handy utility function for that. +We store it as a string as we won't make any further calculations in this example. If the active Tick is initialized, we also need to set the liquidityNet to correctly handle moving out of the position: ```typescript diff --git a/docs/sdk/v3/guides/advanced/04-price-oracle.md b/docs/sdk/v3/guides/advanced/04-price-oracle.md new file mode 100644 index 0000000000..49a4f70cef --- /dev/null +++ b/docs/sdk/v3/guides/advanced/04-price-oracle.md @@ -0,0 +1,4 @@ +--- +id: price-oracle +title: Uniswap as a Price Oracle +--- \ No newline at end of file From a4cd0cc6c28bc9183e40e8b2464be353ddbd3c5a Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Wed, 21 Jun 2023 14:48:15 +0100 Subject: [PATCH 13/52] Add oracle guide --- .../v3/guides/advanced/03-active-liquidity.md | 38 ++- .../sdk/v3/guides/advanced/04-price-oracle.md | 258 +++++++++++++++++- 2 files changed, 281 insertions(+), 15 deletions(-) diff --git a/docs/sdk/v3/guides/advanced/03-active-liquidity.md b/docs/sdk/v3/guides/advanced/03-active-liquidity.md index 4d122b3f58..c5f8d573d2 100644 --- a/docs/sdk/v3/guides/advanced/03-active-liquidity.md +++ b/docs/sdk/v3/guides/advanced/03-active-liquidity.md @@ -5,19 +5,19 @@ title: Active Liquidity ## Introduction -This guide will cover how to fetch and compute the active liquidity in the specific tick ranges of a pool. It is based on the [Liquidity Density example](https://github.com/Uniswap/examples/tree/main/v3-sdk/liquidity-density) and can be seen used in production, albeit in a more sophisticated way, in the [Uniswap Analytics](https://info.uniswap.org/#/pools) website. +This guide will cover how to fetch and compute the active liquidity in the specific Tick ranges of a pool. It is based on the [Liquidity Density example](https://github.com/Uniswap/examples/tree/main/v3-sdk/liquidity-density) and can be seen used in production, albeit in a more sophisticated way, in the [Uniswap Analytics](https://info.uniswap.org/#/pools) website. :::info If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](./01-background.md) page! ::: -In this guide, we will use the Tick data we fetched from the V3 subgraph in the previous guide and compute the active liquidity our Pool can use at each tick. We then use `recharts` to draw a chart that visualizes our Pool's liqudity density. +In this guide, we will use the Tick data we fetched from the V3 subgraph in the previous guide and compute the active liquidity our Pool can use at each Tick. We then use `recharts` to draw a chart that visualizes our Pool's liqudity density. This guide will cover: 1. Getting the tickSpacing and currently active Tick from the Pool 2. Calculating active liquidity from net liquidity -3. Drawing a chart from the tick data +3. Drawing a chart from the Tick data This guide will not cover: @@ -35,16 +35,16 @@ To visualize the distribution of active liquidity in our Pool, we want to draw o ### Initialized Ticks When providing liquidity for a pool, the LP decides the **price range** in which the liquidity should be provided, and the amount of liquidity to be provided. -The pool understands the position as **liquidity between the lower and upper tick**. The Tick Index in this context is a representation of the price between the Pool's assets. +The pool understands the position as **liquidity between the lower and upper Tick**. The Tick Index in this context is a representation of the price between the Pool's assets. Looking at this [visualization](https://www.desmos.com/calculator/oduetjzfp4) of multiple positions in a V3 Pool, we can see that the liquidity available for a swap does not change inside a position, but when crossing into the next position. This is what the **Initialized Ticks** of a Pool represent - they are a representation of the start or end of one or more positions. LiquidityNet1 When entering or leaving a position, its liquidity is added or correspondingly removed from the active liquidity available for a Swap. The initialized Ticks store this change in available liquidity in the `liquidityNet` field. -The change is always stored in relation to the currently active tick - the current price. When the price crosses an initialized Tick, it gets updated and liqudity that was previously added when crossing the Tick would now be removed and vice versa. +The change is always stored in relation to the currently active Tick - the current price. When the price crosses an initialized Tick, it gets updated and liqudity that was previously added when crossing the Tick would now be removed and vice versa. -We already fetched the initialized ticks of our Pool in the [previous guide](./02-pool-data.md). The format we got from the Graph is: +We already fetched the initialized Ticks of our Pool in the [previous guide](./02-pool-data.md). The format we got from the Graph is: ```typescript interface GraphTick { @@ -61,7 +61,7 @@ The active liqudity at the current Price is also stored in the smart contract - ### Tickspacing -Only the ticks with indices that are dividable by the tickspacing of a Pool are initializable. The Tickspacing of the Pool is dependent on the Fee Tier. Pools with lower fees are meant to be used for more stable Token Pairs and allow for more granularity in where LPs position their liquidity. We can get the `tickSpacing`from our pool: +Only the Ticks with indices that are dividable by the tickspacing of a Pool are initializable. The Tickspacing of the Pool is dependent on the Fee Tier. Pools with lower fees are meant to be used for more stable Token Pairs and allow for more granularity in where LPs position their liquidity. We can get the `tickSpacing`from our pool: ```typescript const tickSpacing = pool.tickSpacing @@ -71,11 +71,11 @@ const tickSpacing = pool.tickSpacing For the purpose of visualizing the liquidity density of the Pool, it rarely makes sense to display the full Tick Range of the Pool, as the vast majority of liquidity will be focused in a narrow price range. -Instead, we will display a sensible number of ticks around the current price. +Instead, we will display a sensible number of Ticks around the current price. ## Calculating active liquidity -We know the spacing between ticks and the Initialized Ticks where active liquidity changes. All we have to do is start calculating from the current Tick and iterate outwards. +We know the spacing between Ticks and the Initialized Ticks where active liquidity changes. All we have to do is start calculating from the current Tick and iterate outwards. To draw our chart we want a data structure that looks something like this: ```typescript @@ -98,7 +98,7 @@ const tickIdxToTickDictionary: Record = Object.fromEntries( ) ``` -The ticks variable in this code snippet is the response we got from the V3 Subgraph. +The Ticks variable in this code snippet is the response we got from the V3 Subgraph. We want to mark the Tick closest to the current Price and we want to be able to display the prices at a Tick to the user. We calculate the **initializable Tick** closest to the current price and create the active Tick that we start from: @@ -120,7 +120,8 @@ const activeTickProcessed: TickProcessed = { Here we also calculate the price of the tokens from the tickIdx, the `v3-sdk` exports a handy utility function for that. We store it as a string as we won't make any further calculations in this example. -If the active Tick is initialized, we also need to set the liquidityNet to correctly handle moving out of the position: + +If the **current Tick is initialized**, we also need to set the **liquidityNet** to correctly handle moving out of the position: ```typescript const currentTickInitialized = tickIdxToTickDictionary[activeTickIdx] @@ -130,10 +131,10 @@ if (currentTickInitialized !== undefined) { ``` We now start iterating outwards from the active Tick and compute the active liquidity for each Tick we want to display. The processed Tick is then saved in an Array of `TickProcessed`. -We choose an arbitrary number of ticks we want to display, for this example we calculate 100 Ticks in each direction. +We choose an arbitrary number of Ticks we want to display, for this example we calculate 100 Ticks in each direction. ```typescript -import { TickMath } from '@uniswap/v3-sdk' +import { TickMath, tickToPrice } from '@uniswap/v3-sdk' let previousTickProcessed = { ...activeTickProcessed @@ -186,7 +187,7 @@ for (let i = 0; i < 100; i++) { After we are done calculating the next 100 Ticks after the current Tick, we iterate in the opposite direction for the previous Ticks. Iterating downwards, we need to subtract the net liquidity where we added it when iterating upwards. You can find a full code example in the [Uniswap Example repository](https://github.com/Uniswap/examples/tree/main/v3-sdk/active-liquidity). -We are finally able to combine the previous, active and subsequent ticks: +We are finally able to combine the previous, active and subsequent Ticks: ```typescript const allProcessedTicks = previousTicks.concat(activeTickProcessed).concat(subsequentTicks) @@ -229,6 +230,15 @@ Check out the full [code example](https://github.com/Uniswap/examples/tree/main/ You can also take a look at the [Uniswap Info](https://github.com/Uniswap/v3-info) repository to see a similar chart used in production. +## Locked Liquidity + +If you run the example, you will notice that the chart also displays a custom tooltip with additional information that we didn't touch on in this example. +The total locked liqudity in the tooltip represents the sum of positions in the currency locked at the selected Tick. +It is calculated as the maximum token output of a swap when crossing to the next Tick. +The V3 pool here is initialized with only the liquidity of the current Tick. + +Depending on your use case, it may make sense to display this value. You can find the full code in the [code example](https://github.com/Uniswap/examples/tree/main/v3-sdk/active-liquidity) + ## Next Steps Now that you are familiar with liquidity data, consider checking out our next guide on using Uniswap as a Price Oracle. \ No newline at end of file diff --git a/docs/sdk/v3/guides/advanced/04-price-oracle.md b/docs/sdk/v3/guides/advanced/04-price-oracle.md index 49a4f70cef..b8f4110cdf 100644 --- a/docs/sdk/v3/guides/advanced/04-price-oracle.md +++ b/docs/sdk/v3/guides/advanced/04-price-oracle.md @@ -1,4 +1,260 @@ --- id: price-oracle title: Uniswap as a Price Oracle ---- \ No newline at end of file +--- + +## Introduction + +This guide will cover how to fetch price observations from a V3 pool to get onchain asset prices. +It is based on the [Price Oracle example](https://github.com/Uniswap/examples/tree/main/v3-sdk/price-oracle), found in the Uniswap code examples [repository](https://github.com/Uniswap/example). +To run this example, check out the guide's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/price-oracle/README.md) and follow the setup instructions. + +:::info +If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](./01-background.md) page! +::: + +In this example we will use **ethers JS** to observe the development of a Pool's current tick over several blocks. +We will then calculate the time weighted average price - TWAP, and time weighted average liquidity - TWAL over the observed time interval. + +This guide will **cover**: + +1. Fetching observations +2. Computing TWAP +3. Computing TWAL + +Before diving into this guide, consider reading the theory behind using Uniswap V3 as an [Onchain Oracle](../../../../concepts/protocol/oracle.md). + +For this guide, the following Uniswap packages are used: + +- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) +- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) + +The core code of this guide can be found in [`oracle.ts`](https://github.com/Uniswap/v3-sdk/blob/main/src/entities/oracle.ts) + +## Understanding Observations + +First, we need to create a Pool contract to fetch data from the blockchain. Check out the [Pool data guide](./02-pool-data.md) to learn how to compute the address and create an **ethers Contract** to interact with. + +```typescript +const poolContract = new ethers.Contract( + poolAddress, + IUniswapV3PoolABI.abi, + provider +) +``` + +All V3 pools store observations of the current tick and the block timestamp. +To minimize pool deployment costs, only one observation is stored in the contract when the Pool is created. +Anyone who is willing to pay the gas costs can [increase](../../../../contracts/v3/reference/core/UniswapV3Pool.md#increaseobservationcardinalitynext) the number of stored observations to up to `65535`. +If the Pool cannot store an additional observation, it overwrites the oldest one. + +We create an interface to map our data to: + +```typescript +interface Observation { + secondsAgo: number + tickCumulative: bigint + secondsPerLiquidityCumulativeX128: bigint +} +``` + +To fetch the `Observations` from our pool contract, we will use the [`observe`](../../../../contracts/v3/reference/core/UniswapV3Pool.md#observe)function. +We first check how many observations are stored in the Pool by calling the `slot0` function. + +```typescript +const slot0 = await poolContract.slot0() + +const observationCount = slot0.observationCardinality +const maxObservationCount = slot0.observationCardinalityNext +``` + +The `observationCardinalityNext` is the maximum number of Observations the Pool **can store** at the moment. +The `observatioCardinality` is the actual number of Observations the Pool **has currently stored**. +Observations are only stored when the `swap`function is called on the Pool so it can take some time to write the Observations after the `observationCardinalityNext`was increased. +If the number of Observations on the Pool is not sufficient, we need to call the `increaseObservationCardinalityNext` function and set it to the value we desire. +Keep in mind that this is a write function as the contract needs to store more data on the blockchain and we will have to pay a corresponding gas fee. +We will need a **signer** or **wallet** to achieve that. In this example, we want to fetch 10 observations. + +```typescript +import { ethers } from 'ethers + +let provider = new ethers.providers.WebSocketProvider('rpcUrl...') +let wallet = new ethers.Wallet('private_key', provider) + +const poolContract = new ethers.Contract( + poolAddress, + IUniswapV3PoolABI.abi, + wallet +) + +const txRes = await poolContract.increaseObservationCardinalityNext(10) +``` + +The Pool will now fill the open Observation Slots. +As someone has to pay for the gas to write the observations, the functionality to write to the array of observations is part of the `swap` function of the Pool. + +:::note +Saving an observation is a write operation on the blockchain and therefore costs gas. +This means that the pool will only be able to save observations for blocks where write calls are executed on the Pool contract. +If no Observation is stored for a block, it is calculated as the time weighted arithmetic mean between the two closest Observations. +Because of this, we can be sure the oldest Observation is **at least** 10 blocks old. +It is very likely that the number of blocks covered is bigger than 10. +::: + +## Fetching Observations + +We are now sure that at least 10 observations exist, and can safely fetch observations for the last 10 blocks. +We call the `observe` function with an array of numbers, representing the timestamps of the Observations in seconds ago from now. +In this example, we calculate averages over the last ten blocks so we fetch 2 observations with 9 times the blocktime in between. +Fetching an observation `0s` ago will return the most recent observation interpolated to the current timestamp as observations are written at most once a block. + +```typescript +const timestamps = [ + 0, 108 +] + +const tickCumulatives, secondsPerLiquidityCumulatives = await poolContract.observe(timestamps) + +const observations: Observation[] = timestamps.map((time, i) => { + return { + secondsAgo: time + tickCumulative: BigInt(tickCumulatives[i]) + secondsPerLiquidityCumulativeX128: BigInt(secondsPerLiquidityCumulatives[i]) + } +}) +``` + +We map the response from the RPC provider to match our Observations interface. + +## Calculating the average Price + +To calculate the time weighted average price (TWAP) in the period we fetched, we first need to understand what the values we fetched mean. + +The `tickCumulative` value is a snapshot of the `tick accumulator` at the timestamp we fetched. The Tick Accumulator stores the sum of all current ticks at every second since the Pool was initialised. Its value is therefore increasing with every second. + +We cannot directly use the value of a single Observation for anything meaningful. Instead we need to compare the difference between two Observations and calculate the time weighted arithmetic mean. + +```typescript +const diffTickCumulative = observations[0].tickCumulative - observations[1].tickCumulative +const secondsBetween = 108 + +const averageTick = diffTickCumulative / secondsBetween +``` + +Now that we know the average active Tick over the last 10 blocks, we can calculate the price with the `tickToPrice` function, which returns a [`Price`](../../../core/reference/classes/Price.md) Object. Check out the [Pool data](./02-pool-data.md) guide to understand how to construct a Pool Object and access its properties. + +```typescript +import { tickToPrice, Pool } from '@uniswap/v3-sdk' + +const pool = new Pool(...) + +const TWAP = tickToPrice(pool.token0, pool.token1, averageTick) +``` + +We have now calculated the time weighted average price over the last 108 seconds. + +Let's continue with the average liquidity. + +## Calculating the average Liquidity + +To understand the term **active Liquidity**, check out the [previous guide](./03-active-liquidity.md). +Similar to the `tick accumulator`, the `liquidity accumulator` stores a sum of values for every second since the Pool was initialized and increases with every second. +Because of the size of the active liquidity value, it is impractical to just add up the active liquidity. Instead the seconds per liquidity are summed up. + +The `secondsPerLiquidityX128` value is calculated by shifting the seconds since the last Observation by 128 bits and dividing that value by the active liquidity. It is then added to the accumulator. + +```solidity +uint32 delta = blockTimestamp - last.blockTimestamp; + +uint128 secondsPerLiquidityX128 = (uint160(delta) << 128) / liquidity +uint160 secondsPerLiquidityCumulativeX128 = last.secondsPerLiquidityCumulativeX128 + secondsPerLiquidityX128 +``` + +`last` is the last Observation in this illustrative code snippet. Consider taking a look at the [Oracle library](https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/Oracle.sol) to see the actual implementation. + +Let's invert this calculation and find the average active liquidity over our observed time period. + +```typescript +const diffSecondsPerLiquidityX128 = observations[0].secondsPerLiquidityCumulativeX128 - + observations[1].secondsPerLiquidityCumulativeX128 +const secondsBetweenX128 = BigInt(108) << 128 + +const TWAL = diffSecondsPerLiquidityX128 / secondsBetweenX128 +``` + +This *Time weighted average liquidity* is the harmonic mean over the time period observed. + +## Why prefer observe over observations? + +As touched on previously, the `observe` function calculates Observations for the timestamps requested from the nearest observations stored in the Pool. +It is also possible to directly fetch the stored observations by calling the `observations` function with the index of the Observation that we are interested in. + +Let's fetch all observations stored in our Pool. We already made sure the observationCardinality is 10. +The solidity struct `Observation` looks like this: + +```solidity +struct Observation { + // the block timestamp of the observation + uint32 blockTimestamp; + // the tick accumulator, i.e. tick * time elapsed since the pool was first initialized + int56 tickCumulative; + // the seconds per liquidity, i.e. seconds elapsed / max(1, liquidity) since the pool was first initialized + uint160 secondsPerLiquidityCumulativeX128; + // whether or not the observation is initialized + bool initialized; +} +``` + +It is possible to request any Observation up to (excluding) index `65535`, but indices equal to or greater than the `observationCardinality` will return uninitialized Observations. + +```typescript + +let requests = [] +for (let i = 0; i < 10; i++) { + requets.push(poolContract.observations(i)) +} + +const results = await Promise.all(requests) +``` + +We can only request one Observation at a time, so we create an Array of Promises to get an Array of Observations. + +We already see one difference, to using the `observe` function here. +While `observe` creates an array onchain in the smart contract and returns it, calling `observations` requires us to make multiple RPC calls. + +:::note +Depending on our setup and the Node we are using, either option can be faster, but making multiple RPC calls always has the danger of the blockchain state changing between our calls. +While it is extremely unlikely, it is still possible that our Node updates with a new block and new Observation in between our calls. +Because we access indices of an array, this would give us an unexpected result that we need to handle as an edge case in our implementation. +::: + +One way to handle this behaviour is deploying or [using](https://github.com/mds1/multicall) a Contract with a [multicall](https://solidity-by-example.org/app/multi-call/) functionality to get all observations with one request. + +We map the RPC result to the Typescript interface that we created: + +```typescript +const observations = results.map((result) => { + const secondsAgo = Math.floor(Date.now() / 1000) - Number(result.blockTimeStamp) + return { + secondsAgo, + tickCumulative: BigInt(result.tickCumulative) + secondsPerLiquidityCumulativeX128: BigInt(result.secondsPerLiquidityCumulativeX128) + } +}).sort((a, b) => a.secondsAgo - b.secondsAgo) +``` + +We now have an Array of observations in the same format that we are used to. + +:::note +Because of the way Observations are stored, they are **not sorted**. We need to sort the result by the timestamp. +::: + +The timestamps of the Observations we got are correspondent to blocks where Swaps happened on the Pool. +Because of this, we would need to calculate Observations for specific intervals manually from the surrounding Observations. + +Overall, it is much harder to work with `observations` than with `observe`, and we need to consider multiple edge cases. +For this reason, it is recommended to use the `observe` function. + +## Next Steps + +Now that you are familiar with the Oracle feature of Uniswap, consider checking out the [next guide](./05-range-orders.md) on **Range Orders**. From d30dafc9f120fcd19ae342fcc717cdbe41d720b1 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Thu, 22 Jun 2023 17:11:28 +0100 Subject: [PATCH 14/52] Improve formatting and wording, correct details --- .../sdk/v3/guides/advanced/04-price-oracle.md | 61 ++++++++++++------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/docs/sdk/v3/guides/advanced/04-price-oracle.md b/docs/sdk/v3/guides/advanced/04-price-oracle.md index b8f4110cdf..f9624f89d9 100644 --- a/docs/sdk/v3/guides/advanced/04-price-oracle.md +++ b/docs/sdk/v3/guides/advanced/04-price-oracle.md @@ -18,9 +18,11 @@ We will then calculate the time weighted average price - TWAP, and time weighted This guide will **cover**: -1. Fetching observations -2. Computing TWAP -3. Computing TWAL +1. Understanding observations +2. Fetching observations +3. Computing TWAP +4. Computing TWAL +5. Why prefer observe over observations Before diving into this guide, consider reading the theory behind using Uniswap V3 as an [Onchain Oracle](../../../../concepts/protocol/oracle.md). @@ -44,9 +46,9 @@ const poolContract = new ethers.Contract( ``` All V3 pools store observations of the current tick and the block timestamp. -To minimize pool deployment costs, only one observation is stored in the contract when the Pool is created. +To minimize pool deployment costs, only one Observation is stored in the contract when the Pool is created. Anyone who is willing to pay the gas costs can [increase](../../../../contracts/v3/reference/core/UniswapV3Pool.md#increaseobservationcardinalitynext) the number of stored observations to up to `65535`. -If the Pool cannot store an additional observation, it overwrites the oldest one. +If the Pool cannot store an additional Observation, it overwrites the oldest one. We create an interface to map our data to: @@ -58,7 +60,7 @@ interface Observation { } ``` -To fetch the `Observations` from our pool contract, we will use the [`observe`](../../../../contracts/v3/reference/core/UniswapV3Pool.md#observe)function. +To fetch the `Observations` from our pool contract, we will use the [`observe`](../../../../contracts/v3/reference/core/UniswapV3Pool.md#observe) function. We first check how many observations are stored in the Pool by calling the `slot0` function. ```typescript @@ -69,14 +71,18 @@ const maxObservationCount = slot0.observationCardinalityNext ``` The `observationCardinalityNext` is the maximum number of Observations the Pool **can store** at the moment. -The `observatioCardinality` is the actual number of Observations the Pool **has currently stored**. -Observations are only stored when the `swap`function is called on the Pool so it can take some time to write the Observations after the `observationCardinalityNext`was increased. -If the number of Observations on the Pool is not sufficient, we need to call the `increaseObservationCardinalityNext` function and set it to the value we desire. -Keep in mind that this is a write function as the contract needs to store more data on the blockchain and we will have to pay a corresponding gas fee. -We will need a **signer** or **wallet** to achieve that. In this example, we want to fetch 10 observations. +The `observationCardinality` is the actual number of Observations the Pool **has currently stored**. + +Observations are only stored when the `swap()` function is called on the Pool or when a **Position is modified**, so it can take some time to write the Observations after the `observationCardinalityNext` was increased. +If the number of Observations on the Pool is not sufficient, we need to call the `increaseObservationCardinalityNext()` function and set it to the value we desire. + +This is a write function as the contract needs to store more data on the blockchain. +We will need a **wallet** or **signer** to pay the corresponding gas fee. + +In this example, we want to fetch 10 observations. ```typescript -import { ethers } from 'ethers +import { ethers } from 'ethers' let provider = new ethers.providers.WebSocketProvider('rpcUrl...') let wallet = new ethers.Wallet('private_key', provider) @@ -91,10 +97,10 @@ const txRes = await poolContract.increaseObservationCardinalityNext(10) ``` The Pool will now fill the open Observation Slots. -As someone has to pay for the gas to write the observations, the functionality to write to the array of observations is part of the `swap` function of the Pool. +As someone has to pay for the gas to write the observations, writing to the array of observations is part of the `swap()` and the `modifyPosition()` function of the Pool. :::note -Saving an observation is a write operation on the blockchain and therefore costs gas. +Saving an Observation is a write operation on the blockchain and therefore costs gas. This means that the pool will only be able to save observations for blocks where write calls are executed on the Pool contract. If no Observation is stored for a block, it is calculated as the time weighted arithmetic mean between the two closest Observations. Because of this, we can be sure the oldest Observation is **at least** 10 blocks old. @@ -105,8 +111,9 @@ It is very likely that the number of blocks covered is bigger than 10. We are now sure that at least 10 observations exist, and can safely fetch observations for the last 10 blocks. We call the `observe` function with an array of numbers, representing the timestamps of the Observations in seconds ago from now. + In this example, we calculate averages over the last ten blocks so we fetch 2 observations with 9 times the blocktime in between. -Fetching an observation `0s` ago will return the most recent observation interpolated to the current timestamp as observations are written at most once a block. +Fetching an Observation `0s` ago will return the **most recent Observation** interpolated to the current timestamp as observations are written at most once a block. ```typescript const timestamps = [ @@ -132,7 +139,7 @@ To calculate the time weighted average price (TWAP) in the period we fetched, we The `tickCumulative` value is a snapshot of the `tick accumulator` at the timestamp we fetched. The Tick Accumulator stores the sum of all current ticks at every second since the Pool was initialised. Its value is therefore increasing with every second. -We cannot directly use the value of a single Observation for anything meaningful. Instead we need to compare the difference between two Observations and calculate the time weighted arithmetic mean. +We cannot directly use the value of a single Observation for anything meaningful. Instead we need to compare the **difference** between two Observations and calculate the **time weighted arithmetic mean**. ```typescript const diffTickCumulative = observations[0].tickCumulative - observations[1].tickCumulative @@ -151,7 +158,7 @@ const pool = new Pool(...) const TWAP = tickToPrice(pool.token0, pool.token1, averageTick) ``` -We have now calculated the time weighted average price over the last 108 seconds. +We have now calculated the **time weighted average price** over the last 108 seconds. Let's continue with the average liquidity. @@ -159,7 +166,7 @@ Let's continue with the average liquidity. To understand the term **active Liquidity**, check out the [previous guide](./03-active-liquidity.md). Similar to the `tick accumulator`, the `liquidity accumulator` stores a sum of values for every second since the Pool was initialized and increases with every second. -Because of the size of the active liquidity value, it is impractical to just add up the active liquidity. Instead the seconds per liquidity are summed up. +Because of the size of the active liquidity value, it is impractical to just add up the active liquidity. Instead the **seconds per liquidity** are summed up. The `secondsPerLiquidityX128` value is calculated by shifting the seconds since the last Observation by 128 bits and dividing that value by the active liquidity. It is then added to the accumulator. @@ -170,7 +177,7 @@ uint128 secondsPerLiquidityX128 = (uint160(delta) << 128) / liquidity uint160 secondsPerLiquidityCumulativeX128 = last.secondsPerLiquidityCumulativeX128 + secondsPerLiquidityX128 ``` -`last` is the last Observation in this illustrative code snippet. Consider taking a look at the [Oracle library](https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/Oracle.sol) to see the actual implementation. +`last` is the most recent Observation in this illustrative code snippet. Consider taking a look at the [Oracle library](https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/Oracle.sol) to see the actual implementation. Let's invert this calculation and find the average active liquidity over our observed time period. @@ -182,7 +189,14 @@ const secondsBetweenX128 = BigInt(108) << 128 const TWAL = diffSecondsPerLiquidityX128 / secondsBetweenX128 ``` -This *Time weighted average liquidity* is the harmonic mean over the time period observed. +This **time weighted average liquidity** is the harmonic mean over the time period observed. + +:::note +The costs associated with manipulating/ changing the liquidity of a Pool are **orders of magnitude smaller** than with manipulating the price of the assets, as **prices** will be arbitraged for assets **with more than one market**. +Adding massive amounts of liquidity to a Pool and withdrawing them after a block has passed more or less only costs gas fees. + +Use the **TWAP** with care and consider handling outliers. +::: ## Why prefer observe over observations? @@ -233,11 +247,12 @@ One way to handle this behaviour is deploying or [using](https://github.com/mds1 We map the RPC result to the Typescript interface that we created: ```typescript +const utcNow = Math.floor(Date.now() / 1000) const observations = results.map((result) => { - const secondsAgo = Math.floor(Date.now() / 1000) - Number(result.blockTimeStamp) + const secondsAgo = utcNow - Number(result.blockTimeStamp) return { secondsAgo, - tickCumulative: BigInt(result.tickCumulative) + tickCumulative: BigInt(result.tickCumulative), secondsPerLiquidityCumulativeX128: BigInt(result.secondsPerLiquidityCumulativeX128) } }).sort((a, b) => a.secondsAgo - b.secondsAgo) @@ -246,7 +261,7 @@ const observations = results.map((result) => { We now have an Array of observations in the same format that we are used to. :::note -Because of the way Observations are stored, they are **not sorted**. We need to sort the result by the timestamp. +Because Observations are stored in a **fixed size array**, they are **not sorted**. We need to sort the result by the timestamp. ::: The timestamps of the Observations we got are correspondent to blocks where Swaps happened on the Pool. From 9972fd72e45c85378c0ec025559181d4f4fc10e3 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Fri, 23 Jun 2023 14:27:41 +0100 Subject: [PATCH 15/52] Update to version 3.2.6. of sdk-core --- docs/sdk/core/reference/enums/ChainId.md | 173 ++++++++++++++++++ .../reference/enums/NativeCurrencyName.md | 83 +++++++++ .../core/reference/enums/SupportedChainId.md | 151 --------------- docs/sdk/core/reference/overview.md | 3 +- docs/sdk/v2/guides/01-quick-start.md | 6 +- docs/sdk/v2/guides/02-fetching-data.md | 12 +- docs/sdk/v2/guides/03-pricing.md | 20 +- docs/sdk/v2/guides/04-trading.md | 4 +- docs/sdk/v2/reference/02-pair.md | 6 +- docs/sdk/v2/reference/03-route.md | 6 +- docs/sdk/v2/reference/04-trade.md | 6 +- 11 files changed, 288 insertions(+), 182 deletions(-) create mode 100644 docs/sdk/core/reference/enums/ChainId.md create mode 100644 docs/sdk/core/reference/enums/NativeCurrencyName.md delete mode 100644 docs/sdk/core/reference/enums/SupportedChainId.md diff --git a/docs/sdk/core/reference/enums/ChainId.md b/docs/sdk/core/reference/enums/ChainId.md new file mode 100644 index 0000000000..2d0d8716a0 --- /dev/null +++ b/docs/sdk/core/reference/enums/ChainId.md @@ -0,0 +1,173 @@ +[@uniswap/sdk-core](../README.md) / Exports / ChainId + +# Enumeration: ChainId + +## Table of contents + +### Enumeration Members + +- [ARBITRUM\_ONE](ChainId.md#arbitrum_one) +- [ARBITRUM\_GOERLI](ChainId.md#arbitrum_goerli) +- [CELO](ChainId.md#celo) +- [CELO\_ALFAJORES](ChainId.md#celo_alfajores) +- [GOERLI](ChainId.md#goerli) +- [MAINNET](ChainId.md#mainnet) +- [OPTIMISM](ChainId.md#optimism) +- [OPTIMISM\_GOERLI](ChainId.md#optimism_goerli) +- [POLYGON](ChainId.md#polygon) +- [POLYGON\_MUMBAI](ChainId.md#polygon_mumbai) +- [SEPOLIA](ChainId.md#sepolia) +- [GNOSIS](ChainId.md#gnosis) +- [MOONBEAM](ChainId.md#moonbeam) +- [BNB](ChainId.md#bnb) +- [AVALANCHE](ChainId.md#avalanche) + +## Enumeration Members + +### ARBITRUM\_ONE + +• **ARBITRUM\_ONE** = ``42161`` + +#### Defined in + +[chains.ts:7](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L7) + +___ + +### ARBITRUM\_GOERLI + +• **ARBITRUM\_GOERLI** = ``421613`` + +#### Defined in + +[chains.ts:8](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L8) + +___ + +### CELO + +• **CELO** = ``42220`` + +#### Defined in + +[chains.ts:11](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L11) + +___ + +### CELO\_ALFAJORES + +• **CELO\_ALFAJORES** = ``44787`` + +#### Defined in + +[chains.ts:12](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L12) + +___ + +### GOERLI + +• **GOERLI** = ``5`` + +#### Defined in + +[chains.ts:3](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L3) + +___ + +### MAINNET + +• **MAINNET** = ``1`` + +#### Defined in + +[chains.ts:2](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L2) + +___ + +### OPTIMISM + +• **OPTIMISM** = ``10`` + +#### Defined in + +[chains.ts:5](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L5) + +___ + +### OPTIMISM\_GOERLI + +• **OPTIMISM\_GOERLI** = ``420`` + +#### Defined in + +[chains.ts:6](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L6) + +___ + +### POLYGON + +• **POLYGON** = ``137`` + +#### Defined in + +[chains.ts:9](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L9) + +___ + +### POLYGON\_MUMBAI + +• **POLYGON\_MUMBAI** = ``80001`` + +#### Defined in + +[chains.ts:10](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L10) + +___ + +### SEPOLIA + +• **SEPOLIA** = ``11155111`` + +#### Defined in + +[chains.ts:4](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L4) + +___ + +### GNOSIS + +• **GNOSIS** = ``100`` + +#### Defined in + +[chains.ts:13](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L13) + +___ + +### MOONBEAM + +• **MOONBEAM** = ``1284`` + +#### Defined in + +[chains.ts:14](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L14) + +___ + +### BNB + +• **BNB** = ``56`` + +#### Defined in + +[chains.ts:15](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L15) + +___ + +### AVALANCHE + +• **AVALANCHE** = ``43114`` + +#### Defined in + +[chains.ts:16](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L16) diff --git a/docs/sdk/core/reference/enums/NativeCurrencyName.md b/docs/sdk/core/reference/enums/NativeCurrencyName.md new file mode 100644 index 0000000000..c0c6cb50a2 --- /dev/null +++ b/docs/sdk/core/reference/enums/NativeCurrencyName.md @@ -0,0 +1,83 @@ +[@uniswap/sdk-core](../README.md) / Exports / NativeCurrencyName + +# Enumeration: NativeCurrencyName + +## Table of Contents + +### Enumeration Members + +- [ETHER](NativeCurrencyName.md#ether) +- [MATIC](NativeCurrencyName.md#matic) +- [CELO](NativeCurrencyName.md#celo) +- [GNOSIS](NativeCurrencyName.md#gnosis) +- [MOONBEAM](NativeCurrencyName.md#moonbeam) +- [BNB](NativeCurrencyName.md#bnb) +- [AVAX](NativeCurrencyName.md#avax) + +### ETHER + +• **ETHER** = ``ETH`` + +#### Defined in + +[chains.ts:38](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L38) + +___ + +### MATIC + +• **MATIC** = ``MATIC`` + +#### Defined in + +[chains.ts:39](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L39) + +___ + +### CELO + +• **CELO** = ``CELO`` + +#### Defined in + +[chains.ts:40](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L40) + +___ + +### GNOSIS + +• **GNOSIS** = ``XDAI`` + +#### Defined in + +[chains.ts:41](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L41) + +___ + +### MOONBEAM + +• **MOONBEAM** = ``GLMR`` + +#### Defined in + +[chains.ts:42](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L42) + +___ + +### BNB + +• **BNB** = ``BNB`` + +#### Defined in + +[chains.ts:43](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L43) + +___ + +### AVAX + +• **AVAX** = ``AVAX`` + +#### Defined in + +[chains.ts:44](https://github.com/Uniswap/sdk-core/blob/main/src/chains.ts#L44) diff --git a/docs/sdk/core/reference/enums/SupportedChainId.md b/docs/sdk/core/reference/enums/SupportedChainId.md deleted file mode 100644 index aa9bdab0d7..0000000000 --- a/docs/sdk/core/reference/enums/SupportedChainId.md +++ /dev/null @@ -1,151 +0,0 @@ -[@uniswap/sdk-core](../README.md) / [Exports](../modules.md) / SupportedChainId - -# Enumeration: SupportedChainId - -## Table of contents - -### Enumeration Members - -- [ARBITRUM\_ONE](SupportedChainId.md#arbitrum_one) -- [ARBITRUM\_RINKEBY](SupportedChainId.md#arbitrum_rinkeby) -- [CELO](SupportedChainId.md#celo) -- [CELO\_ALFAJORES](SupportedChainId.md#celo_alfajores) -- [GOERLI](SupportedChainId.md#goerli) -- [KOVAN](SupportedChainId.md#kovan) -- [MAINNET](SupportedChainId.md#mainnet) -- [OPTIMISM](SupportedChainId.md#optimism) -- [OPTIMISM\_GOERLI](SupportedChainId.md#optimism_goerli) -- [POLYGON](SupportedChainId.md#polygon) -- [POLYGON\_MUMBAI](SupportedChainId.md#polygon_mumbai) -- [RINKEBY](SupportedChainId.md#rinkeby) -- [ROPSTEN](SupportedChainId.md#ropsten) - -## Enumeration Members - -### ARBITRUM\_ONE - -• **ARBITRUM\_ONE** = ``42161`` - -#### Defined in - -[constants.ts:10](https://github.com/Uniswap/sdk-core/blob/9997e88/src/constants.ts#L10) - -___ - -### ARBITRUM\_RINKEBY - -• **ARBITRUM\_RINKEBY** = ``421611`` - -#### Defined in - -[constants.ts:11](https://github.com/Uniswap/sdk-core/blob/9997e88/src/constants.ts#L11) - -___ - -### CELO - -• **CELO** = ``42220`` - -#### Defined in - -[constants.ts:19](https://github.com/Uniswap/sdk-core/blob/9997e88/src/constants.ts#L19) - -___ - -### CELO\_ALFAJORES - -• **CELO\_ALFAJORES** = ``44787`` - -#### Defined in - -[constants.ts:20](https://github.com/Uniswap/sdk-core/blob/9997e88/src/constants.ts#L20) - -___ - -### GOERLI - -• **GOERLI** = ``5`` - -#### Defined in - -[constants.ts:7](https://github.com/Uniswap/sdk-core/blob/9997e88/src/constants.ts#L7) - -___ - -### KOVAN - -• **KOVAN** = ``42`` - -#### Defined in - -[constants.ts:8](https://github.com/Uniswap/sdk-core/blob/9997e88/src/constants.ts#L8) - -___ - -### MAINNET - -• **MAINNET** = ``1`` - -#### Defined in - -[constants.ts:4](https://github.com/Uniswap/sdk-core/blob/9997e88/src/constants.ts#L4) - -___ - -### OPTIMISM - -• **OPTIMISM** = ``10`` - -#### Defined in - -[constants.ts:13](https://github.com/Uniswap/sdk-core/blob/9997e88/src/constants.ts#L13) - -___ - -### OPTIMISM\_GOERLI - -• **OPTIMISM\_GOERLI** = ``420`` - -#### Defined in - -[constants.ts:14](https://github.com/Uniswap/sdk-core/blob/9997e88/src/constants.ts#L14) - -___ - -### POLYGON - -• **POLYGON** = ``137`` - -#### Defined in - -[constants.ts:16](https://github.com/Uniswap/sdk-core/blob/9997e88/src/constants.ts#L16) - -___ - -### POLYGON\_MUMBAI - -• **POLYGON\_MUMBAI** = ``80001`` - -#### Defined in - -[constants.ts:17](https://github.com/Uniswap/sdk-core/blob/9997e88/src/constants.ts#L17) - -___ - -### RINKEBY - -• **RINKEBY** = ``4`` - -#### Defined in - -[constants.ts:6](https://github.com/Uniswap/sdk-core/blob/9997e88/src/constants.ts#L6) - -___ - -### ROPSTEN - -• **ROPSTEN** = ``3`` - -#### Defined in - -[constants.ts:5](https://github.com/Uniswap/sdk-core/blob/9997e88/src/constants.ts#L5) diff --git a/docs/sdk/core/reference/overview.md b/docs/sdk/core/reference/overview.md index b0fff92b54..4599eca308 100644 --- a/docs/sdk/core/reference/overview.md +++ b/docs/sdk/core/reference/overview.md @@ -9,7 +9,8 @@ title: Overview ### Enumerations - [Rounding](enums/Rounding.md) -- [SupportedChainId](enums/SupportedChainId.md) +- [ChainId](enums/ChainId.md) +- [NativeCurrencyName](enums/NativeCurrencyName.md) - [TradeType](enums/TradeType.md) ### Classes diff --git a/docs/sdk/v2/guides/01-quick-start.md b/docs/sdk/v2/guides/01-quick-start.md index 5f8134040a..404dfd06af 100644 --- a/docs/sdk/v2/guides/01-quick-start.md +++ b/docs/sdk/v2/guides/01-quick-start.md @@ -16,9 +16,9 @@ To run code from the SDK in your application, use an `import` or `require` state ## ES6 (import) ```typescript -import { SupportedChainId } from '@uniswap/sdk-core' +import { ChainId } from '@uniswap/sdk-core' import {Pair} from '@uniswap/v2-sdk' -console.log(`The chainId of mainnet is ${SupportedChainId.MAINNET}.`) +console.log(`The chainId of mainnet is ${ChainId.MAINNET}.`) ``` ## CommonJS (require) @@ -26,7 +26,7 @@ console.log(`The chainId of mainnet is ${SupportedChainId.MAINNET}.`) ```typescript const CORE = require('@uniswap/sdk-core') const V2_SDK = require('@uniswap/v2-sdk') -console.log(`The chainId of mainnet is ${CORE.SupportedChainId.MAINNET}.`) +console.log(`The chainId of mainnet is ${CORE.ChainId.MAINNET}.`) ``` # Reference diff --git a/docs/sdk/v2/guides/02-fetching-data.md b/docs/sdk/v2/guides/02-fetching-data.md index 4cf1f7497d..e1285810dd 100644 --- a/docs/sdk/v2/guides/02-fetching-data.md +++ b/docs/sdk/v2/guides/02-fetching-data.md @@ -29,9 +29,9 @@ The next piece of data we need is **decimals**. One option here is to simply pass in the correct value, which we may know is `18`. At this point, we're ready to represent DAI as a [Token](../../core/reference/classes/Token.md): ```typescript -import { SupportedChainId, Token } from '@uniswap/sdk-core' +import { ChainId, Token } from '@uniswap/sdk-core' -const chainId = SupportedChainId.MAINNET +const chainId = ChainId.MAINNET const tokenAddress = '0x6B175474E89094C44Da98b954EedeAC495271d0F' // must be checksummed const decimals = 18 @@ -41,9 +41,9 @@ const DAI = new Token(chainId, tokenAddress, decimals) If we don't know or don't want to hardcode the value, we could look it up ourselves via any method of retrieving on-chain data in a function that looks something like: ```typescript -import { SupportedChainId } from '@uniswap/sdk-core' +import { ChainId } from '@uniswap/sdk-core' -async function getDecimals(chainId: SupportedChainId, tokenAddress: string): Promise { +async function getDecimals(chainId: ChainId, tokenAddress: string): Promise { // Setup provider, import necessary ABI ... const tokenContract = new ethers.Contract(tokenAddress, erc20abi, provider) return tokenContract["decimals"]() @@ -55,7 +55,7 @@ async function getDecimals(chainId: SupportedChainId, tokenAddress: string): Pro Finally, we can talk about **symbol** and **name**. Because these fields aren't used anywhere in the SDK itself, they're optional, and can be provided if you want to use them in your application. However, the SDK will not fetch them for you, so you'll have to provide them: ```typescript -import { SupportedChainId, Token } from '@uniswap/sdk-core' +import { ChainId, Token } from '@uniswap/sdk-core' const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin') ``` @@ -79,7 +79,7 @@ The data we need is the _reserves_ of the pair. To read more about reserves, see One option here is to simply pass in values which we've fetched ourselves to create a [Pair](../reference/pair). In this example we use ethers to fetch the data directly from the blockchain: ```typescript -import { SupportedChainId, Token, WETH9, CurrencyAmount } from '@uniswap/sdk-core' +import { ChainId, Token, WETH9, CurrencyAmount } from '@uniswap/sdk-core' import { Pair } from '@uniswap/v2-sdk' const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) diff --git a/docs/sdk/v2/guides/03-pricing.md b/docs/sdk/v2/guides/03-pricing.md index 6ac5a2e169..f1d2ab211d 100644 --- a/docs/sdk/v2/guides/03-pricing.md +++ b/docs/sdk/v2/guides/03-pricing.md @@ -18,13 +18,13 @@ Let's consider the mid price for DAI-WETH (that is, the amount of DAI per 1 WETH The simplest way to get the DAI-WETH mid price is to observe the pair directly: ```typescript -import { SupportedChainId, Token, WETH9 } from '@uniswap/sdk-core' +import { ChainId, Token, WETH9 } from '@uniswap/sdk-core' import { Route } from '@uniswap/v2-sdk' -const DAI = new Token(SupportedChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) +const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) // To learn how to get Pair data, refer to the previous guide. -const pair = await createPair(DAI, WETH9[SupportedChainId.MAINNET]) +const pair = await createPair(DAI, WETH9[ChainId.MAINNET]) const route = new Route([pair], WETH9[DAI.chainId], DAI) @@ -43,17 +43,17 @@ Finally, you may have noticed that we're formatting the price to 6 significant d For the sake of example, let's imagine a direct pair between DAI and WETH _doesn't exist_. In order to get a DAI-WETH mid price we'll need to pick a valid route. Imagine both DAI and WETH have pairs with a third token, USDC. In that case, we can calculate an indirect mid price through the USDC pairs: ```typescript -import { SupportedChainId, Token, WETH9} from '@uniswap/sdk-core' +import { ChainId, Token, WETH9} from '@uniswap/sdk-core' import { Route, Pair } from '@uniswap/v2-sdk' -const USDC = new Token(SupportedChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6) -const DAI = new Token(SupportedChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) +const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6) +const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) // To learn how to get Pair data, refer to the previous guide. -const USDCWETHPair = await createPair(USDC, WETH9[SupportedChainId.MAINNET]) +const USDCWETHPair = await createPair(USDC, WETH9[ChainId.MAINNET]) const DAIUSDCPair = await createPair(DAI, USDC) -const route = new Route([USDCWETHPair, DAIUSDCPair], WETH9[SupportedChainId.MAINNET], DAI) +const route = new Route([USDCWETHPair, DAIUSDCPair], WETH9[ChainId.MAINNET], DAI) console.log(route.midPrice.toSignificant(6)) // 1896.34 console.log(route.midPrice.invert().toSignificant(6)) // 0.000527331 @@ -66,10 +66,10 @@ Mid prices are great representations of the _current_ state of a route, but what Imagine we're interested in trading 1 WETH for DAI: ```typescript -import { SupportedChainId, Token, WETH9, CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { ChainId, Token, WETH9, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { Route, Pair, Trade } from '@uniswap/v2-sdk' -const DAI = new Token(SupportedChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) +const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) // To learn how to get Pair data, refer to the previous guide. const pair = await createPair(DAI, WETH9[DAI.chainId]) diff --git a/docs/sdk/v2/guides/04-trading.md b/docs/sdk/v2/guides/04-trading.md index 7a4338bb4f..b8ace2a2ad 100644 --- a/docs/sdk/v2/guides/04-trading.md +++ b/docs/sdk/v2/guides/04-trading.md @@ -14,10 +14,10 @@ This guide will focus exclusively on sending a transaction to the [latest Uniswa Let's say we want to trade 1 WETH for as much DAI as possible: ```typescript -import { SupportedChainId, Token, WETH9, CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { ChainId, Token, WETH9, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import {Trade, Route} from '@uniswap/v2-sdk' -const DAI = new Token(SupportedChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) +const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18) // See the Fetching Data guide to learn how to get Pair data const pair = await createPair(DAI, WETH9[DAI.chainId]) diff --git a/docs/sdk/v2/reference/02-pair.md b/docs/sdk/v2/reference/02-pair.md index 8e9deee3be..151355896a 100644 --- a/docs/sdk/v2/reference/02-pair.md +++ b/docs/sdk/v2/reference/02-pair.md @@ -13,10 +13,10 @@ The Pair entity represents a Uniswap pair with a balance of each of its pair tok ```typescript import { Pair } from '@uniswap/sdk-core' -import {SupportedChainId, Token, CurrencyAmount } from '@uniswap/v2-sdk' +import {ChainId, Token, CurrencyAmount } from '@uniswap/v2-sdk' -const HOT = new Token(SupportedChainId.MAINNET, '0xc0FFee0000000000000000000000000000000000', 18, 'HOT', 'Caffeine') -const NOT = new Token(SupportedChainId.MAINNET, '0xDeCAf00000000000000000000000000000000000', 18, 'NOT', 'Caffeine') +const HOT = new Token(ChainId.MAINNET, '0xc0FFee0000000000000000000000000000000000', 18, 'HOT', 'Caffeine') +const NOT = new Token(ChainId.MAINNET, '0xDeCAf00000000000000000000000000000000000', 18, 'NOT', 'Caffeine') const pair = new Pair(CurrencyAmount.fromRawAmount(HOT, '2000000000000000000'), CurrencyAmount.fromRawAmount(NOT, '1000000000000000000')) ``` diff --git a/docs/sdk/v2/reference/03-route.md b/docs/sdk/v2/reference/03-route.md index 0ed78f1698..ba76a43e5d 100644 --- a/docs/sdk/v2/reference/03-route.md +++ b/docs/sdk/v2/reference/03-route.md @@ -12,11 +12,11 @@ The Route entity represents one or more ordered Uniswap pairs with a fully speci # Example ```typescript -import { SupportedChainId, Token, CurrencyAmount } from '@uniswap/sdk-core' +import { ChainId, Token, CurrencyAmount } from '@uniswap/sdk-core' import { Pair, Route } from '@uniswap/v2-sdk' -const HOT = new Token(SupportedChainId.MAINNET, '0xc0FFee0000000000000000000000000000000000', 18, 'HOT', 'Caffeine') -const NOT = new Token(SupportedChainId.MAINNET, '0xDeCAf00000000000000000000000000000000000', 18, 'NOT', 'Caffeine') +const HOT = new Token(ChainId.MAINNET, '0xc0FFee0000000000000000000000000000000000', 18, 'HOT', 'Caffeine') +const NOT = new Token(ChainId.MAINNET, '0xDeCAf00000000000000000000000000000000000', 18, 'NOT', 'Caffeine') const HOT_NOT = new Pair(CurrencyAmount.fromRawAmount(HOT, '2000000000000000000'), CurrencyAmount.fromRawAmount(NOT, '1000000000000000000')) const route = new Route([HOT_NOT], NOT, HOT) diff --git a/docs/sdk/v2/reference/04-trade.md b/docs/sdk/v2/reference/04-trade.md index 13373f7ce7..bf59be2fdb 100644 --- a/docs/sdk/v2/reference/04-trade.md +++ b/docs/sdk/v2/reference/04-trade.md @@ -12,11 +12,11 @@ The Trade entity represents a fully specified trade along a route. This entity s # Example ```typescript -import { SupportedChainId, Token, CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { ChainId, Token, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { Pair, Trade, Route } -const HOT = new Token(SupportedChainId.MAINNET, '0xc0FFee0000000000000000000000000000000000', 18, 'HOT', 'Caffeine') -const NOT = new Token(SupportedChainId.MAINNET, '0xDeCAf00000000000000000000000000000000000', 18, 'NOT', 'Caffeine') +const HOT = new Token(ChainId.MAINNET, '0xc0FFee0000000000000000000000000000000000', 18, 'HOT', 'Caffeine') +const NOT = new Token(ChainId.MAINNET, '0xDeCAf00000000000000000000000000000000000', 18, 'NOT', 'Caffeine') const HOT_NOT = new Pair(CurrencyAmount.fromRawAmount(HOT, '2000000000000000000'), CurrencyAmount.fromRawAmount(NOT, '1000000000000000000')) const NOT_TO_HOT = new Route([HOT_NOT], NOT, HOT) From 1836aa743c7bd0270532b68cf2fe8fe25a3418e7 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Fri, 23 Jun 2023 16:54:40 +0100 Subject: [PATCH 16/52] Correct wrong calculation in TWAL section --- docs/sdk/v3/guides/advanced/04-price-oracle.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/sdk/v3/guides/advanced/04-price-oracle.md b/docs/sdk/v3/guides/advanced/04-price-oracle.md index f9624f89d9..85fc4ad9bb 100644 --- a/docs/sdk/v3/guides/advanced/04-price-oracle.md +++ b/docs/sdk/v3/guides/advanced/04-price-oracle.md @@ -29,7 +29,6 @@ Before diving into this guide, consider reading the theory behind using Uniswap For this guide, the following Uniswap packages are used: - [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) -- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) The core code of this guide can be found in [`oracle.ts`](https://github.com/Uniswap/v3-sdk/blob/main/src/entities/oracle.ts) @@ -120,7 +119,7 @@ const timestamps = [ 0, 108 ] -const tickCumulatives, secondsPerLiquidityCumulatives = await poolContract.observe(timestamps) +const [tickCumulatives, secondsPerLiquidityCumulatives] = await poolContract.observe(timestamps) const observations: Observation[] = timestamps.map((time, i) => { return { @@ -186,7 +185,7 @@ const diffSecondsPerLiquidityX128 = observations[0].secondsPerLiquidityCumulativ observations[1].secondsPerLiquidityCumulativeX128 const secondsBetweenX128 = BigInt(108) << 128 -const TWAL = diffSecondsPerLiquidityX128 / secondsBetweenX128 +const TWAL = secondsBetweenX128 / diffSecondsPerLiquidityX128 ``` This **time weighted average liquidity** is the harmonic mean over the time period observed. @@ -222,7 +221,6 @@ struct Observation { It is possible to request any Observation up to (excluding) index `65535`, but indices equal to or greater than the `observationCardinality` will return uninitialized Observations. ```typescript - let requests = [] for (let i = 0; i < 10; i++) { requets.push(poolContract.observations(i)) From ed145ff01c5376169df3c4bf474d0ef0d1ad7a2b Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Thu, 29 Jun 2023 14:01:20 +0100 Subject: [PATCH 17/52] Correct price oracle, add range order --- .../sdk/v3/guides/advanced/04-price-oracle.md | 2 +- .../sdk/v3/guides/advanced/05-range-orders.md | 408 ++++++++++++++++++ .../v3/guides/advanced/images/range-order.png | Bin 0 -> 105903 bytes 3 files changed, 409 insertions(+), 1 deletion(-) create mode 100644 docs/sdk/v3/guides/advanced/05-range-orders.md create mode 100644 docs/sdk/v3/guides/advanced/images/range-order.png diff --git a/docs/sdk/v3/guides/advanced/04-price-oracle.md b/docs/sdk/v3/guides/advanced/04-price-oracle.md index 85fc4ad9bb..a3753ff177 100644 --- a/docs/sdk/v3/guides/advanced/04-price-oracle.md +++ b/docs/sdk/v3/guides/advanced/04-price-oracle.md @@ -30,7 +30,7 @@ For this guide, the following Uniswap packages are used: - [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) -The core code of this guide can be found in [`oracle.ts`](https://github.com/Uniswap/v3-sdk/blob/main/src/entities/oracle.ts) +The core code of this guide can be found in [`oracle.ts`](https://github.com/Uniswap/examples/tree/main/v3-sdk/price-oracle/src/libs/oracle.ts) ## Understanding Observations diff --git a/docs/sdk/v3/guides/advanced/05-range-orders.md b/docs/sdk/v3/guides/advanced/05-range-orders.md new file mode 100644 index 0000000000..6602fb776b --- /dev/null +++ b/docs/sdk/v3/guides/advanced/05-range-orders.md @@ -0,0 +1,408 @@ +--- +id: range-orders +title: Range Orders +--- + +## Introduction + +This guide will cover how single-side liquidity provisioning can be used to execute Limit Orders on Uniswap V3 Pools. +An example to showcase this concept can be found in the [Range Order example](https://github.com/Uniswap/examples/tree/main/v3-sdk/range-order), in the Uniswap code examples [repository](https://github.com/Uniswap/example). +To run this example, check out the guide's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/price-oracle/README.md) and follow the setup instructions. + +:::info +This guide builds on top of the [Pooling Liquidity guides](../liquidity/01-minting-position.md). +We recommend going through this section of the docs before imnplementing Range Orders. +::: + +In this example we will create a single-side liquidity position with the [NonfungiblePositionManager](../../../../contracts/v3/reference/periphery/NonfungiblePositionManager.md) contract. +We will then use **ethers JS** to observe the price of the Pool on new blocks and withdraw the liquidity when our target is reached. + +This guide will **cover**: + +1. Understanding Range Orders +2. Calculating our Tick Range +3. Creating a single-side liquidity position +4. Observing the price of the Pool +5. Closing the Limit Order + +Before working through this guide, consider checking out the Range Orders [concept page](../../../../concepts/protocol/range-orders.md) to understand how Limit orders can be executed with Uniswap V3. + +For this guide, the following Uniswap packages are used: + +- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) +- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) + +The core code of this guide can be found in [`range-order.ts`](https://github.com/Uniswap/examples/tree/main/v3-sdk/range-order/src/libs/range-order.ts). + +## Understanding Range Orders + +If you have read the [Range Order Concept page](../../../../concepts/protocol/range-orders.md), you can skip this section. + +Positions on a V3 Pool are always created with a Tick range in which their liquidity is accessible to swaps on the Pool. +Lets look at the return value of the NonfungiblePositionManager contract when calling the `positions` function with a Position `tokenId`. + +```solidity +function positions( + uint256 tokenId + ) external view returns ( + uint96 nonce, + address operator, + address token0, + address token1, + uint24 fee, + int24 tickLower, // Lower Boundary of Position + int24 tickUpper, // Upper Boundary of Position + uint128 liquidity, // Liquidity + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1) +``` + +We see that a position only stores a single `liquidity` value, and a `tickLower` and `tickUpper` value that define the range in which the liquidity of the Position can be utilised for Swaps. +The actual **amount** of `token0` and `token1` that a Pool owes the Position owner is calculated from the parts of the liquidity position that are to the left and right of the current Tick. +Liquidity left of the current Tick is denominated in `token0` and liquidity right of the current Tick is denominated in `token1`. + +If a new Position is created and the Tick Range of the position does not include the current Tick of the Pool, only one of the two Tokens in the Pool can be provided. + +We will call this a **Single Side Liquidity Position**. + +RangeOrder + +When the current Tick of the Pool moves across the Position, the ratio of `token0` and `token1` will change, and ultimately inverse if the current Tick moves out of the position on the other side. + +We will utilise this behaviour to provide liquidity with `token1` and withdraw the position when it has been converted to `token0`. + +## Calculating the Tick Range + +Our goal for this guide is to create a [Take Profit Order](../../../../concepts/protocol/range-orders.md#take-profit-orders) that trades `token0` for `token1` when the Price of `token0` increases by 5%. +To create our Position, we need to first decide the Tick Range that we want to provide liquidity in. + +### Upper Tick + +We [create a Pool](./02-pool-data.md) that represents the V3 Pool we are interacting with and get the `token0Price`. +We won't need full tick data in this example. + +```typescript +import { Pool } from '@uniswap/v3-sdk' + +... +const pool = new Pool(token0, token1, fee, sqrtPriceX96, liquidity, tickCurrent) + +const currentPrice = pool.token0Price +``` + +Next we increase the `Price` by 5%. We create a new Price with a numerator 5% higher than our current Price: + +```typescript +import { Price, Fraction } from '@uniswap/sdk-core' + +const targetFraction = Price.asFraction.multiply(new Fraction(100 + 5, 100)) + +const targetPrice = new Price( + currentPrice.baseCurrency, + currentPrice.quoteCurrency, + targetFraction.denominator, + targetFraction.numerator +) +``` + +Be aware that the `numerator` and `denominator` parameters are ordered differently in the `Fraction` and `Price` constructor. + +We have calculated our target Price but we still need to find the nearest tick to create our Position. + +:::info +As Positions can only start and end at initializable Ticks of the Pool, so we can only create a Range Order to a Price that exactly matches a Tick. +::: + +We use the `priceToClosestTick` function to find the closest tick to our targetPrice. +We then use the `nearestUsableTick` function to find the closest initializable Tick for the `tickSpacing` of the `Pool`. + +```typescript +import {priceToClosestTick, nearestUsableTick} from '@uniswap/v3-sdk' + +let targetTick = nearestUsableTick( + priceToClosestTick(targetPrice), + pool.tickSpacing +) +``` + +This nearest Tick will most likely not exactly match our Price target. + +Depending on our personal preferences we can either err on the higher or lower side of our target by adding or subtracting the `tickSpacing` if the initializable Tick is lower or higher than the theoretically closest Tick. + +### Lower Tick + +We now find the lower Tick by subtracting the tickSpacing from the upper Tick: + +```typescript +let lowerTick = targetTick - pool.tickSpacing +``` + +If the price difference is too low, the lower tick may be left of the current Tick of the Pool. +In that case we would not be able to provide single side liquidity. +We can either throw an Error or increase our Position by one Tick. + +```typescript +if (tickLower <= pool.tickCurrent) { + tickLower += pool.tickSpacing + targetTick += pool.tickSpacing +} +``` + +We now have a lower and upper Tick for our Position, next we need to construct and mint it. + +## Creating the Single Side Liquidity Position + +We will use the `NonfungiblePositionManager` and `Position` classes from the `v3-sdk` to construct our position. We then use an **etherJS** wallet to mint our Position on-chain. + +### Minting the Position + +We create a `Position` object with our ticks and the amount of tokens we want to deposit: + +```typescript +import { Position } from '@uniswap/v3-sdk' + +const position = Position.fromAmount0({ + pool: pool, + tickLower: tickLower, + tickUpper: targetTick, + amount0: amount, + useFullPrecision: true +}) +``` + +Before we mint our position, we need to give the `NonfungiblePositionManager` Contract an approval to transfer our tokens. +We can find the Contract address on the official [Uniswap Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). + +```typescript +import ethers from 'ethers' + +const provider = new ethers.providers.JsonRpcProvider(rpcUrl) +const wallet = new ethers.Wallet(privateKey, provider) + +const tokenContract = new ethers.Contract( + pool.token0.address, + ERC20_ABI, + wallet +) + +await tokenContract['approve']( + NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, + ethers.BigNumber.from(amount) + ) +``` + +Once we have our approval, we create the calldata for the **Mint** call using the `NonfungiblePositionManager`: + +```typescript +import {MintOptions, NonfungiblePositionManager} +import { Percent } from '@uniswap/sdk-core' + +const mintOptions: MintOptions = { + recipient: wallet.address, + deadline: Math.floor(Date.now() / 1000) + 60 * 20, + slippageTolerance: new Percent(50, 10_000), +} + +const { calldata, value } = NonfungiblePositionManager.addCallParameters( + order.position, + mintOptions +) +``` + +We can populate our mint transaction and send it with our wallet: + +```typescript + const transaction = { + data: calldata, + to: NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, + value: ethers.BigNumber.from(value), + from: address, + maxFeePerGas: MAX_FEE_PER_GAS, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS, + } + +const txRes = await wallet.sendTransaction(transaction) +``` + +### Getting the tokenId + +We want to read the response to our `Mint` function call to get the position id. +We wait for the transaction receipt and fetch the result using `trace_transaction`: + +```typescript +let receipt = null +let mintCallOutput + +while (receipt === null) { + try { + receipt = await provider.getTransactionReceipt(txRes.hash) + + if (receipt === null) { + continue + } else { + const callTraces = await provider.send('trace_transaction', [ + txRes.hash + ]) + mintCallOutput = callTraces[0].result.output + } + } catch (e) { + break + } +} +``` + +Your Node provider may not support this call. In that case you can also call the NonfungiblePositionManager Contract with the wallet address and identify the Range Order Position manually. +We decode the result with the **ethers AbiCoder**. The solidity function has this signature: + +```solidity +function mint( + struct INonfungiblePositionManager.MintParams params +) external returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) +``` + +So we need the first parameter to get the `tokenId`: + +```typescript +const decodedOutput = ethers.utils.defaultAbiCoder.decode( + ['tuple(uint256, uint128, uint256, uint256)'], + mintCallOutput + )[0] + +const tokenId = decodedOutput.toNumber() +``` + +We have created our Range Order Position, now we need to monitor it. + +## Observing the Price + +We need to observe the price of the Pool and withdraw our Position once the `tickCurrent` has moved across our Position. + +We use **ethers JS** to watch for new blocks and fetch the latest Pool data: + +```typescript + +provider.on('block', refreshPool()) + +function refreshPool() { + + ... // construct Pool contract + + const slot0 = await poolContract.slot0() + const tickCurrent = slot0.tick +} +``` + +It is not necessary to calculate the Price from the tick we fetched, as executing the limit order is dependent on the tick range we defined and not the Price from which we calculated it. + +```typescript + +if (tickCurrent > targetTick) { + // Withdraw position +} +``` + +We check if the tick has crossed our position, and if so we withdraw the Position. + +## Closing the Limit Order + +We call the NonfungiblePositionManager Contract with the `tokenId` to get all info of our position as we may have gotten fees from trades on the Pool: + +```typescript +import INON_FUNGIBLE_POSITION_MANAGER from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json' + +const positionManagerContract = new ethers.Contract( + NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, + INONFUNGIBLE_POSITION_MANAGER.abi, + provider +) + +const positionInfo = await positionManagerContract.positions(tokenId) +``` + +We use the `NonfungiblePositionManager`, the `pool`, `positionInfo` and `tokenId` to create call parameter for a `decreaseLiquidity` call. + +We start with creating `CollectOptions`: + +```typescript +import { Percent, CurrencyAmount } from '@uniswap/sdk-core' +import { CollectOptions, RemoveLiquidityOptions } from '@uniswap/v3-sdk' +import JSBI from 'jsbi' + +const collectOptions: Omit = { + expectedCurrencyOwed0: CurrencyAmount.fromRawAmount( + pool.token0, + JSBI.BigInt(positionInfo.tokensOwed0.toString()) + ), + expectedCurrencyOwed1: CurrencyAmount.fromRawAmount( + pool.token1, + JSBI.BigInt(positionInfo.tokensOwed1.toString()) + ), + recipient: wallet.address, +} +``` + +Next we create `RemoveLiquidityOptions`. We remove all our liquidity so we set liquidityPercentage to `1`: + +```typescript +const removeLiquidityOptions: RemoveLiquidityOptions = { + deadline: Math.floor(Date.now() / 1000) + 60 * 20, + slippageTolerance: new Percent(50, 10_000), + tokenId, + // percentage of liquidity to remove + liquidityPercentage: new Percent(1), + collectOptions, + } +``` + +We create a new `Position` object from the updated `positionInfo` info we fetched: + +```typescript + +const updatedPosition = new Position{ + pool, + liquidity: JSBI.BigInt(currentPositionInfo.liquidity.toString()), + tickLower: currentPositionInfo.tickLower, + tickUpper: currentPositionInfo.tickUpper, +} +``` + +We have everything to create our calldata now and are ready to make our Contract call: + +```typescript + +const { calldata, value } = NonfungiblePositionManager.removeCallParameters( + updatedPosition, + removeLiquidityOptions + ) +const transaction = { + data: calldata, + to: NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, + value: value, + from: address, + maxFeePerGas: MAX_FEE_PER_GAS, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS, +} + +const result = await wallet.sendTransaction(transaction) +``` + +Our liquidity position is removed and we receive `token1` at the Price we have specified. +We have successfully executed a range order. + +## Caveats + +Executing a range order has certain limitations that may have become obvious during the course of this guide. + +- If the price of the Pool drops below `tickUpper` while we already decided to withdraw our liquidity our order may fail and we either receive `token0`, `token0` and `token1` or our transaction fails depending on our exact implementation. +- Range Orders can only be created between initializable ticks and may not exactly represent our limit order Price-Target. +- Depending on the price ratio of the tokens in the Pool the minimum price difference to the current price may be significant. +- The tokens received are the average between the Price of `tickUpper` and `tickLower` of the Range order. This can be a significant difference for Pools with a tickCurrent far from 0, for example tokens with different decimals (WETH/ USDT, WETH/USDC). The example showcases this behaviour well with the default configuration. + +## Next Steps + +This guide showcases everything you need to implement Range Orders on your own but only demonstrates creating a Take Profit order in `token0` to `token1` direction. +Consider implementing Buy Limit orders as described in the [Range Orders concept page](../../../../concepts/protocol/range-orders.md#buy-limit-orders). + +This is currently the last guide in the `v3-sdk` series. Consider joining the [Uniswap Discord](https://discord.com/invite/ybKVQUWb4s) or checkout the official [Github](https://github.com/Uniswap) to learn more about the Uniswap Protocol. diff --git a/docs/sdk/v3/guides/advanced/images/range-order.png b/docs/sdk/v3/guides/advanced/images/range-order.png new file mode 100644 index 0000000000000000000000000000000000000000..9e2091d48061206d8b1f72f47484dba442c91b4d GIT binary patch literal 105903 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�Sc}rj&bP{NV9IS`O|HHbQP%LBq~NT58Pxg; z+$lbbJ~mW69I1JMNU*WEUm1>p7o^96vywY0T@v>g46lTY6xgXokTj%vEGHkOw7p$+ z#=3Wxk*s8ROsR}Xh5ANYb;u?jo(;#C`~ud^>wyTH5Qw^q8A8as(8X}Od3aRkE+wV$ zmjXB;QvZA#&cu8j6PXVdx1N_mnAG&jH3Zl~^CZ0@W<(OhJ)yu{T3=av*Jj2rrmmtz&`5~QG!=M7# z_Nm`@HBn9ry&A86Nccyd^QKP06_X_1Fco_*t#&c!U^W3ZlGgIlD}2j6;Sn`DZzF)a zXuEMQ?BO}G56;QAtH%;soTpa5p4a{27 zkjt$+q7LoV#?|x&101|5ns5Jx5JyA%1*rg29PNDV0CTf%@`X~W^Xw*d6tNl1R$v?< zzmf{Sa@~b^2O8wyfGH?^|5BwIEx51e(f^}K=6 zrEktL5=ejB3q;A*23$7zi_z1+h>nFB-WZLciE|U;`b;XT|4}#H(w9Bj;Xo1(vds>H zd&bu1kQN$_7yn~i%VaI^X1O-VshhdtW)pvEmU``k(Q=1n3g6bk%aKY6EuvTGu)cPt ze0H>xj4u+`GF))TOj9%J?W!T~Ef%DL;|^rhMnubWzcQQeH|W%Lw@+D!d3)nb12`0( zt7zwJ9Scps6mBdlkZF>%&>@}6Qdp=8-cyf7+%^DWY)0p{6y(_NKcfUpVgwBHA64pR z#o)1*rgIm{o4eT`@<_jpL|Zz?!svB#fyl1NATyAUE7RhdzWyvd{I5kWL}~2Qz7~tc z=E-RtbHo{Xhh(5)iV}lR;+^alcb-QB8GI`1vV7WEAn-k=6e(A@7y5-8=rPI6Byf*Z zSTfVa*CQn4H`Ql%^kpMjZNRxYqgy!B+^41{FPP_zZrbhDX7m`S#r$l<6p}G#6P2|^mKfdld*w!Ws`4QSL*;H(LH~cpm9wfCui~dMCa8P zssvjjR+`>R_d*=6huL0@=yX=1J+=nEnWFx&fR|66dq(b(qc+`LqeoZC(c}UmnRo7x zV_8L9*5RDqbw_BatQ5xM)%hyX*Qrs@atW5d?I4VE`A+Tq-%N5Ka%ID;68Cq zP%akj#$)u|vt<)}$6Cl9p@VR~G#l)prpWoZ5Ox+Tw7Ak!># z7fw+`i%UVW7ZzSeuq+d8WPcdX86MkyE2zvgb+mI+IZfdUDfW60nBG-$rQ;VF$FLi2D*3DNp+{(C zvG|K7g^=}Fqx)mTtGG2~+r#ksC}OTBa<(sLB}ljHZe;PYBtrvuMO6Op7RC~p8|NH-Mud;U%iLA&f<02zu6d9LQ_6i|nlU+h2gv>n<#U}J+Y*>+Yw=51ZB9b=q5DlsntI8O%<4XIf5GUl zm={ZSB$ET-&Kqwz&56WpmkFtL)2!1Y%bjlLymbESWBix_d7yMjg0$#l;+N03^&}1X z$rV&o(r&%D;vigu+_X2L*sK91iT+*%+~iJie;d-e)*9&@Y4lfr{jbfPX_B{XDQ2mu z*KQ%V%)~|i>U8P^m)|Ec(fdqGZ$?2y$b~7DqLNIwvz1`Uw*SCAwr^ZCgG^`qz|EV`tp}){C<+N%6%1YafMzQ29cEErTUcA z=r>%6Hg8dSzoBm@yFE1Iq`lVdWs#?(85|;Y??g%zO+5 z^5}H$N0ELyyX@^{*u!&C?G%#%|NH5l|0@jJ_N0l`YP|&VHVyX}o@41>h|!=gj3<%$u@|gyJAr=Cr$6BI(WEgG)jXcwHc@xu z+^L!RU1jC>ag{n7XN^?D_-&m?`p&mBK)G$WDz$4C1|e)l5Wo-6k{oAqI?2pgaBt+=j& z7@5rAC^CWdhc|j_1`F!dnCdMq6HGDo`pH!HkzVQeWs$*Ru42o_JxE%W)*ui?o~pD? zN1|t)$f7${!TYc4!C4s4Au#{-GUqrBbQVVE?Y5#)GoQV-6|ezyL*!uwtSgKBneE^*ppH<8{IUjc`$5ka^{t2d=a zIvEpNUtIj@Oyj<{)$SLOeJ%e(N*t#w5vSaZ9hr{*5hERa*8bFgVnf?hLN}tu>bKc1@Y8q z-*8`Nij2C+{@L2zO$LW4O&V?Judii8F}~%Y^nWv`zic+fb{Im0dsBR@6LkJ8QG=6xND~i zFcv2*5w#Zu^=E~oGM{HY=mX#R2TS{1O_@?_}&!FT3_;di!A{Q=<>hOl8R1IbtyZ*NrtS&sG<}GCDT%~;a_e<(-|DB;v+{?;!{bM&2f9bYE zLepTD4???P#ETF%NW)Rk4Wk8g$mN^t5>$j}>I9FL=N=>>%&XaqJD9%tv+DW}lUZ+U zQO%?86Q7-tkzKmKA)=jkuB?)=lfN=p16^ScX);fkYU?8E#qM8PbgZa@=lfABW)t$vk!2Qa+f{+tIj{0fL<&EAW?!C{ z_CK?}g5y;#nDYt|u7MhWfa;)*J=IhU8To@4mjh(Jz}PVt`ln)JX?$6Q6m`2j zgLvbrJXbt*LDB9h~Q`rgjPgm$G-?y z>ds(RCTcGC%3$xFJ}V(K7@Y^x)7P3A*QgX`%RA3~0P~=2Q#vT-p(DToo{f+yKad$C z+Es+w1ENn4=c~!#S#4{f<$_I&q$8&;IQ-n`?ks6c`}8UA@vG!y)zqq_G@h!w)gKI~ zI%cxXiUEY<18Z(=$m7*;iE-pLe_-AzM=?<+rFF!x#k{`$#HdG8=C|&Dcr9xW4mI@f zy`-%_j?L$uvsjFECi(Q&Pg2Bo>CjJmwohIM^-1IXix$XC7XGQ(`jwIoF|x-@9&Q}( zF^V=hvB`yv&wn)82rzbJuy2i63{d9bf1tR(RjPniA)PUA;>KmZQEEYv+0J7BrSM#t z4z(=XO6cj1Y9A@0IT6$fc8jhcokLrfI}W8{+x+&NX1X*psM@X|+ELIMuKupB&yMHe ztH>?&2fqM<1p_@WanAJPaFwryN(-l6*L(Yo$)TS-NvL|1esJO1{-(gqKl7(nduI~h zwHbS=9oMa@t(NwBkd4c*BF;XJsx2|(cst{7Ca84xz`#!H^ko&|t}dI(Y-iOwM${Fl zUP4c!_>*D?7F6A4L_;{UjE`6-RT7YtK&lx88ZznCi(FTxzMa!urPWj#R#&D+yQD1Z zM-}9b?kYc^?%?b2BW^CZSO})3F4tEnnas_Wgr_lou4>-4UN;U};8H2SX?*<56?3L{Y4h^xRfW1BO=I7m6^T2OLBf|VSmVAX9DkR zUzP7K8-Xl33x=!Y%pxDVHRSFab%OxTXlx1CW2Sjy`QYXwtsDTbTC5O?oJj}=ajku05CnB$^p$ zP7yx_0;jSEP{HU+ka?X4YXOV6lj46qjN4K*!(f2~s7GS{T0}ezzg|%*7|#&=3n(0O zY3`^g(yLmZF@0R#rWtJg+Dd>lKUgzX<$F>4sz!dkSWDtL-_>%&2HvZoo+wxxzxvMn z0}%-0FTGo}s%`>{_@_@i{+{YuTGRj*>{?ZPH{vb(p;2DSIsRrrGi5_GF-?JU@ARlZ zV1!0`!EAFOU77c`4G4xwA$e2;IW!1H!dAf-@qsJxuJ&p%I{re8Gz}PT&VfmpX}QUN zr1;JgFk9l`z;y`E!2bgZ#dF%5_#~31Vs~$yYwQHw)n>`NK{=hneENcHeaW=4kZv!& zv_6`E`*gy|Pkt*TZ61g3J@R)>dskCakREkJ&@$Iwh!a9P8@0P1(G=1#?bj2Y~^a47B{fIz=!0>L#RCI$pj zMj?Cn;Pe?W62v~P%AFRf6v;q%PSbIs()J5Y^NQj-&pZX90FSaq8fRqW&8-Ef|Du&| z6niOoLkl~LPWRP}I@$wownqcey&`0#9Q8!)1xogpX-+Li9*8vyz`QOn_s()ZcN(v}VG2G$fmyiSK<8}AT?j^bXyJf#PA?bMIQ9@vuO|gBs2^Lc6OA9ko;PG4V?Pi|O4|p5DNKsErn999SFPj3Rh)8r zN4I6Z?M4qkHBRv4>>T|vul9#e+WaL6F@Be=*(5bA7Zf5?y(VY$v#Y+l_jfjC*ubMz zR(+*q0e;1%e@i&eU8xDVGii!8wWKHqdhdiyh5eFO8YM;TlN$M*n6q5M#9t>@AA&s1 z60DWrH~fvR;O=JQLp6aXFlcQboJ{zYapPFfY~iw(>8KUk+JFxFG79l-Aqx@#Q5?Pc zBcmFB5YeN-*YG)6lo;`w1EgZ!AthxkJnt}+-Y{ESj;m=QP?TwYCUUF&K=S1wl%)qH zS&Lw5X$P_fct+vIY=I5;@f4>T-m>T;@G~HWldW-w(r;k;?nIDIiKdU3-zYz81Cx2_ z$X|H%H^I8}ckQcf&p;CMHfDFZO0wK9$n7GCWrZh0uiKd0`RR(e zO|ij%1+*D>1I4C9Up5*>@+c+Yt8g8e6ja1@<2!l0l5*!e!LI3foVn&#+bidE3{LsW zLJ{~e`8kZ28WU0hwm8*L)2o3vnGkZ@)G)jfV^TKY zAVVD6IL#Uo9DFE;sLyLP%UiQYuBdLf&m1>>_;{{^%_{-j>!QmfYNGSxool>( zuGSbU)4NrjE1I1s(@IAvMKb=|MC<*FwZGo#&;IO>uSb|(5UkC=W+@podI=+#Qc1UY z1S;4H7P1(~h73!6!J4R#ErQU<^ZmZ6hmF(fg9VikCvWFJ3~%{>C%jXfW^$4pb)wb#~we zl0t#a*XS^O8C4=Na(xS?E5GJdo^$npcWEi!A^(|-wswby~^93$fl}-o#?LCmzBkVfPf%6RHn;E93n!x!@f~`zSB!0?6%&*Gurf+)r_8LB6~GL zH}Q4!eR*OMKb8=N?m#rA+qv?450lyUIRDW^is`e?yA9cSA-}x` zu%DSOyoFe`w7LT>h{YmglSEjUK5P(BhGW#bHMG_E10eIfS-K5{5hBaq_f}RWng*3b z>#FYL&Z0GMRGN%$^|WSIH6>KyLfB-{iwG$qXl$wM1BImr$GG~=XrdTnlH^C0+q7GL z1a9P|$)}k5U4|yBB~J5Nd3uFLs?t?B4U{ z8t1HoIlNZz{R!V66@p}Zy1vF4m^8v5N(zOt00^A3P5yuru6^*jd85SJc?R;gL4X0*#p04Ek{P*GX?YmFALV7Xd8?{^N3*+ z?y2QN&b;)ytyxl(nqKo&#zB9Vrjvm%ah1XsBb*4le%%TyNIP{VH*)43mLm?K`@w)f z%@Wd@{~r4`IobUpo}J<^1>=WLa!MK?ilgB!A5`e5>TASv=_KT?&96MI0Ch>RI|F&) zi}bwi+d8N6PxYs&V;cP~{I-|Jcd@VLNDCS4iS5>*9|ZlKD0xzb^=c$n`|1V0URc=f zB@Z^%9H!Yu(zG_f8>|RfCH}@53Nm#618VUB1RnylUzL;RH{9g^`I_MN=sfZ;+ovbQ z#l`j6t|yDx(IDB!A-3gjRkaRwmtNt-yN_UKu)Fa7U4=_tXs9X8N1kHS26a2-GZCJ* ze5$tuW>emvHYZl%Z>7*{+E`bWj(3l?Nbdb?WN>&AIPq0yzL!n(jv>8%|EVb436ciG zURqR?YdyEx6&tfU*mQ*-9ePEv$(}>V#mL5VvfYE>h2gSE6xcn_A&BYzfq`Ow2&{{J z{blAKLOZoz-%@)D9#|ZldLTG}k#yOE_S*;?AHEs1 z@DMYGO$Z^!6&+?*3;=rWp@*w2Ip6@k#;C{1;w&D1Exscj#foAG99a?tPRpd<_GgL1 z%@4u=3yfkB@9oz3*>xT?Y<6xL_6<~7H^=fJQU-N$Wkt}lOhgdliaO*mPF3ZI_u3Y7 z()b9UoZeBjFm0O)|2ETS6w$mSMSUWzDcvqn} zx4{)@22MX53+%3OnHes;r(v+^69MsVC_vv7=(~ji>oZf(p8N)B$;bb4u%W^ykb_wQ zw)Wmcb+O519??2S1aMLIa7o+W)WW?HAQs(;-h(X{+?{8@0Zj>n+IFPLQYd!_w!^xeAm-K0;OtBqdwlw7Uc;JzMzMwfUHc*0T8vy1ok zb$j*ZyVPRYsV5w3}lgvE9YYs4Ht7*Y-h5e6vC{0IZY z)I)pi@_K5piQ7c_J=u$H7m0^FwwbuBxjp?k3Ji8}?ygoETjT%EN^xl)ST8H|qB!twoujWVlUJCF_%0nVboCntUsxF^W51UFNz-eP1RoSMKwy-F}U#isxB zDTu;mvmyl*VAtOoX*s6H*#xwwIn2XwR(%gBSfzBon3aL!-0DLms<}cGSmmIYB;jyg$;SFqLRfHFbCm0r3J= zVc^IX$z1^6fuJ7fz|V?wA-CYal{HwOpf7D<*zV1~3WCb~FI`%44|ukSlLIN|UlgQ7 zsd2vME11*(nL;Jv+8DYAsXhMfSGQtSYgzCR34>S;fH64`^+rDwt`4(_Wd< z!f~=z_?wTt&&~4$kQvs{rEzGi_OEi3-3MuZZ$Ze6UT^NI7 z8=6|6fx|V~TNL%BySx8@Z|T5B?-GsHi-F`e5)S2r>e6wkba#a=BbAW8mR>qsB@qRv z5XSi|gz}~TT(?8ajkVv&($YGjStBcZrPOVsDX;C}K!G!|!l+Mh=Ox~SgoEt(_G;sw z&6(AVp3+e2H+?!2OSV@#67sOugCjH3P%6Yj>yu8xS5Kw*w!tR<11xrZ!xza`plA10 zlo$8d_nW!`Ez-?Ss|Qe zqGVk?H6x=mrgYa6>Pq723H>SBBHyY+e`k0AT4RLr*_;T#V9rdBdkntWh1 z^+r+64t+g2JAMzW^}n|ed~REtLL($W2#>+tnt>`sY6)42z!HomKIbL(q!=uZ1_%7> zsomoqIPKN`{n@Jfb&`%~yYaR!f>|Wg`vE4?LQ+X|yhmd;r8WX|%vwC;Dzvemj1==T zIo6LXZMd%-=};xe0%Z?uXh03!gsgiGmenbYhNkou0dh!0(;@WJ24b)e-{N^TAOQ2r z{Jg0AjhN&#wf%ILYP_tb&gJ6=Zjq9C3)ggyuIXW)KD>(bIzi1$_lhG`HiT3ql?`*y zo?nWPjgc1_^7E1Un$l$Vhz8kp2#v)Vg-arUC$mO!>8tQ@MLPn^J$(IU?Fl=0 z;boraS2MUx?!jlw&T?eR3|m%RzVP5r^tVQvTeC&74X*^i|1kZ~X40 zNu21jyPD4?3q4Ya<-9jL@g&_4?{iQw2w+SM3Oj8zgd_}*Heh$VH9}Wy*aLDP%D~YU z8MiYbiulpX$G(8Qx{*Io(oBp%P>D`6nGxQsS?fIO7L{&0d0(9aZ~+k%r3%u4vHlpl zxTLD@n(A>7{X)k`-{od(LpI%SXoJA6_ZQ#zk1au3@$T1?SH%d2W#8m|yES*_L#(3K z7%Ug}9H)j~qn0siE*@$-O`?gSH>lBq=*;{6(fG%w+0#lpT+i$J@hY`z(0|gQI;>O5 zzv1s>3LC4$w@})mYtIpdE5If0!8@$gTz;XU9~`eFT3}ZBNfordp{FRZ$m>Bz%gck z`K<-4Gw#Pnk7i)}vA=~hd$4Xj0yVS19_fTOl$Ne8;<+>5+x+Hpdn^1xG*7v}KL&x# z3FQ+AJ7w-Gdj5@>)o{AOXCO>MISD7uh~iC@R0DmSO)L{=f5SRX=BVi%{3bmQ?LTkehoPtU z{*H4KeNq|@g5?+)n?u0+&_;M&IRF)P6w>YSEK9};YUmo902LUge02vfG%PxAZ@vdc zMug)5bbC&_%gWTOA+ZZkZ`m<+Hr$m4MPf53w=|dvorO`c#B3lwIOO;Og^n*j`{F)> zlRod4p0c9;wbKVsYisFBk-Dzd*E zLkvoz*EOe}g}o5k%tHfq1y}QZ58#M_-^26{^k_5VN$+Hc9S=K%iK!A4vAaK8s>~X{ z17`!RJeD*h8_0S;D)`MNUsr6kYC%sPgov(zAs(MMkhS;$_9k z?TJkVXWZRh3E-bARJ-IpEx(d-aN3?p>tizgm!E%_k<1#72RMZsK7r86p!+f`^>|;( zyo~_)#QRl%Jf?$knHTxXt~hSXUq8Bn(4&B6&}<{)(t!uIv(OIh0dkxZfZCwe4X)FQ zP^3%R^rk*VzP~qB5+T<45Adn*A^^a`wVY(W9%)AG9&EfpJx_V8`u{A4GO;@9N|SIo zkXj}{+IqVJnU3j9^REXQzyn6II|<NQ` zN&;&RhXB>IT)sCOd{-XTSpMa&>)!TMF!q1D9+Q&i+}S9i`B^M84<3(8J3UZ(@R1QD zEOT>MA8>pTu##FKlY9ybeA#l16e|szx@nra+JMLTP{=tetdw`jy}jCvKjszkhI5XYZFOmyq3%gkY(<3TDiV!q`G^I+L zcF&yi-_e@#a4U11Z4H7{F^eKlO+gEEW2IGUaqx!-W$ zV`zIOZy9-tNLwFaenzA)zN8WQ5KC>ZTmD>7EJ8Fg{182iJEXBWEWV&Jav<8GW@$aA zR5;GZ{@ieYB}uk($D)S_b{6#JwOn20VDjx|fGy8l47g61A-gEpcl$cU){N5}8x!RI(Z zOnyf9J9Zt6>YZyfGwXNKU%u2cpYh~~C@(3946sP?XYY75OlY^8<2p^1r1h9;`TaX^>u^T^%}^5i$DVq1QJ+TO59Eno4Wkp%^`XL z5)xsXTAJgZ+T-VyjC0cb{)OHJ3Bo6#mKk;tGFg*m^YJ23X(Z4mtl)1lv;Lh$6p?r z+vEawE)j*Q0$p89#X$PIwD2}tazDwQ-!~fLG%YA+1;k8gzxE*6JiYZgUArQ^u#6Yn zo(Ir8!k241m@@&Y>3;XM?c-X=)V0-CkUtPO864+RYl8jX{tnXtU~F|jpDY|gd?I`= zSJhPG-!hBos~2MxD^G!HBok)Q=-ugNWYaSpF&DoeVkJ$bdf_=vS>pN3Gu`4} zYo}j7qB@YbZ~URhoP6+*zwqJjc(I)dN02DH8DOobz7FP zw=T)r*#3OyA(%t7V=sR(Xfe$)&dMNh|d1P zK?LeTT+$RfpT5MAOf&!5Tukwl61!&IM1riQk}uttrk{kd+S5UknJ}a*D~X8H-d|ty z1u&%rWNQ1<&2ikC)o%s#yL}*ceil!I#7AIW^E4T&LY=uwxVjNJ;gKKEoN(yeov0P6P(pu?R;%hT!4)P zOSTrB%ABaNsAbS^<_$tZhXR#tQQ2nK!GH2U6c#yqZgfHScT$K(`;=%6Cp;=-uYkEv z(UBT2?KE2rv{}3Wpz#0#@c~g=C>0!#fb0+(4bVl!t%S{#5wfWAD@@g(YrrWl+zKY@ zf@q1JxTUQI{gs-vc`Uz2jBCqxqThp(y6pi^6e=DDY8xDfeml;dd;AuZ?#=Gc)Obsm zyY;0K_LzG2mksm?87B$bT72Cum|xOKAr6!Nc0V`TnW(DOKHqF8y7^p|&02pkLNkhu zl{J?rqDT!aqK@akgfW`lnu{r9^%qe5Tz|X`q?D|_ADUjAyX<8d*{5lKFgI=Rz056O z^=DOlCWXsQ&x&->0U!?Z%!Q1AXjmCfV4f)t@vs#RWYPi>334)>u8t#cB^P}UhM{qa ztVxJU*u_2MG!{6C<#x*>|G`qzmYe!j2|t@ZzOw~rPmUPwo4!tR#pUv?$x~wUc=Xe{ zlCiPolLWM7n*5c_6L;Ft1u}Xle7=@qZNYPB@`5_t_pE!Ty06O(b?NXv zc=ib>#u2d>Zh)9CF2qCE4oVK)dECp8Xh0Jr;b0F=<&#g7^1b@0ncFe9;#(^#T0mKD z>*r-w1AAl2_b-H0D7$&>IJP1IFp z@Aq9K@Jj3l{S$mfgz7x7JpItHU82pT6Fb_4kI~7G^+Nzz<_M+>L<;6zFSw9158=cE z&_?xFW+?=-|LAzy%D?|v`Q!dIAo~^MbE8E?e$f!)mv3TDtQSJXM<(>HLQi(|aZ&W5v;^H-4 z@?9(=ymj;(56b`mp)BoK`I=i>?ijBz_S7P-Q>V%|Yb2j*n;ku_GI)3I(XSH&UIc~R z;Zw@$F~bjk&7J2XdBBQv;uUd4jEF$bzb>0DYOCQz{**33A*%Z#9W6J;J)1fys?`$B z@t)+i%_CvT+nCwJ@nJGOz2~9@O{tGhy}?s=XZll;Ey^&q_Hz2#V<<+l>iG&$ADu4(amMcdM%Thb7i<`r+or zs|2^uvtLxFHHth|0&Oluc?u@ef0r_Q^+YJ*HH}7yVgCvQf_QDrK?zCag-;m%Ht*;M z6))w^u!q&4W8c-NzAgHB4xymEi0GD4kbNp#Ds-yeG*w6>t}Y|*2XB)RA=X%F41%+; z>kkzK>NH~P$qKCwrvEc4epH#mH*^)t+0ku%;C$2K74U6KHO1ccV4(%`@kw7um zsPtXlVDsfRZc8e2IuBbofyYEmV}393%58?ePFgC<$<)`xb>AltmOyHA z;XN$#5fm^?|J10GZWrP3ytT3vusu;B4j5zW&cZddPO=*|Ol}7nsd6+k<0L4)_mT%; zmCf6IDH^SLM4l83wU;gF8}i$H4GHBI`i|{VtmKqMd)ud+{kZpC4$o#cAuzlgcI-6w zu>Z}dpK5!C>K&0|7*mHA!vE(l(K#fvBqi#gA&-8~vJ+vFUa$z4wvcWh%yeMjbLez#PV;FpS+x!u!a zLsK1ZeWqq_zb&XLx_vPXoi;}%<4J&j8s7JAvgPoIznT)KK7294<2dGjM%R^oVx~;4 zWt(F#_oLSk6egaHvK~g;!Cp{h3I;{4L;u0(g-S36Bs)?#rGcEnA zQ%rNF#mv}tw_b>7qSa>5?|h$OuIZb2!@ht7kUUCClWJq5W>+rMV3NPE4NmMa8Bd~^ zsT;~Yij2#FEhqm+Fl}@wYjxvt-beg<&AlA%8us@~vRkB_=#!oQ9yTEU|JaR6dtumPFK5nGyyxa---oIahzN7w&(Wpx7G1 z*xREM9I=BWmum@DKiJ2JRoC!Ug{FZ`U)$h&owk z=@S`rV#RTipZlkKjTAn<-DOZ1_g7O*t{RO)wne`v4%S#R@58#|fL7NPyVfkLUY&#i z&jhKuC;ni#L&%O)xm}korU({(B7^)W>bf^Ns zYQ;dcqo_R#F_m#zd4wlvhSA~@V1h1{W+-}1TpT~Kye~DVJ)8W1Kot>(TR6PJ+(`9Y_Z#IBmQ6|SuB7ozXHfI0wf6Sl+XKPpAR1?6?OASKyK%o`oN&O z*u>Q&a?NxReY7Gqba(l+Gu7v!@fYvWT2J}vr_5}0Lk{?c(ysA1*AwIxsEsakwjgZd z{F_Fs+D=2_IHk1eKk=))N^OccNQ`gB6J?U9W}Gbc{Es4m%r;T7fwIJP`tRD^f*ul; zg9bbDGiL7a}=I4WAF+K@i~2N?eUjJOvSkQd0unf zsOxjo95lfVkf2fC)0s#Kuno3-_@DqYDQ6UcoK%yw_-lQGD4;W;?vgyyXrbXLk81f? zmdDSf5gQ6{0J#M6{}ONXOK=m%H@@x*@q(ZOG#~n-NlS{+^1liH$b+%k%ao_}{No?* zEKrIrCYdT71_|-M&7s#0WvMokWwc3(_`5ZQ{a8Hi%Et1=f8tfbbjQ|IG{<)1njiJ1 zXGcLejh>JgS-XS$i81uIeWA@WFBkpA-lC~GjCz;b{H%0661Xnb&9LR(!%(=> z@KU$IZ|0#kw`DT5)$=PqEe?<&-H>=ZZ899CPb!7!|6N+Fo1t+ZdqT3n_djhtENvqJ z-+#WkcalX#Nc4Ir1Me2%AhPqy-R~=bf!+C7K-Wx2U^{F&DmtV z|LB1R$uHImCb!TQ*45>A4c>n_WwfiCXi}9RiS?dUO2&26r8jYMEAk#3;=@x3v!jIT z%j0yS5|Jv-u2d#5@T-~cTSl#J>p^t-;XyRk{saW+# zTT3EgjKGv=%e|lJf{|XXHYBDkN2pBm56YkHM|uAI^PBNNL0YqW8OQa(sW0ZzZnJtg0p@j+dQEF;8Qzp6=xQ72kPzl9o`A{){BTtkqgp8$3_XNm`+&_d&VCCBoTZ@!ukN zEa^pm$IR6GZ3__*NvqmLd#alolhU_I$yI};tsPBqz4^LkIpH- zdxn2;&HDQ+^X*$sQwtgyres_q$mVAvEx!2uoaqhfO1-CskxB@IxT}k|-lgS!Nv-Q53-{LxNeu>sZ zqP&_?#j5S^NAW$b#*wh)g|A^cc*oHk-%#=N+1WLN_2m_+YjU@|mmK(0H?Ft-SsVd5 zGD6m^)z8R}=C}}$v?A<4X!Tzwir0>Q$skOcwBcG*klPe@2d(#cowM`pXc0hl~W*^WExGez1w7yXv6)A?JjnT>7XC` zEN-`65A}Jj!!)rTf(H)(-KQJT&t=q5U(VL;xnPU}q1Gt0^Yq{N{gleuE^c55o7 zy@s_?FYAOz)5X18x8c7CNyg?++RHaS5`UU)EaB=EK%dvlX}gIJx@IZln0mNpVN-M* z1z7+_MN_wBJP8a`yZGit2On33hjnSGdSs*a1=OC3LtW|qr^$?iA;%Kt)+qF6J2KsG z33ZyKwB0IZFf?|ueo6h&&+&xf!@1LBjB~+qQO&7Jug?Rw!E#tFxf?3iEL~a&<7$>$ z!M|eApXZuptzQ?U;n>f8(P}3<<;%r#{OfR{cJfbae$Pa5DE0-0R-%~S*Pbh8s4ME5)zrdBb zQg4Au_q90E6xk`NauE|1mYw{KwhPxw zFW7A6*Pzw_kKTWU{A`kC^m{np5V}>ekEc=x=_uj6*go+^v}8vkA9;sm?wgL4g{RJW zHPp&f+9{+f20>^Yj<2*0BHc!3k`jZRnC^34E&O;l4gcX4{Rl=htbDYgLTdq)cxA+T z;nzQAlqhRHA&1W?2Ny2-9a6l#b#$iTm&Zl#6C8}y_g%*%VTHfK5w4+s&QK-c4$c~b zldc_WTs{K9xDhn0!Y~h&#xVnRGiXhL^8b{KJHOUnNdD-G`~V(K(7;6!Y%;Yy=ELGL z-y4oQL`C!xSMXQ6FE=>$-h0KI4sfk83-d3U(FE^%NwRm(kI^E@TmhzFJ2XUBuiah3 zurTj|ID_CJGftU{KLx|(LxHE2da-{4VM)+k!@5nau?UNVh`o$#m7b+=Ci4( zdh_0>H`k~~y_?{g?7mo8q0WY3Y+3anSTm`TnY}x8HShZ9f%S>?yj#RIxErO%ttUM} z@}>wCRaE8j;jNdFr`CO~S2w{+W!ivj<1Zu)h%Ik^?q)p>ypZ#*bL#tqie(%`_3otx zi*j+m79#BCtnO?we~iWAM|WA}&Zrb&yb_@}<=V=5UU9y33vVR?I!kqx%8%XofQjH2 zikUk*f-0-w_rwv@Yyj$BdM;zDj%|gZ!7onkP4KAs<&eh%F6%5_{mC$|7 zCj@v_oDu9feXME} zq_qj!My*0Ufp>854v@J4Vm*xw*AlOJ=HHt8Dc77!q%alpi`P`?-6ON?=smWYg7NX; z(~|Q}jlubW`E-M3NjHz3(-TRhB>rU%$A(4;ga- z4MyW)>5K9Fd5S9zU89-I7XIk2l&TYY?28yiJU`?6{f76*)x6KQVg#PnFTIe}MBkr5 z4?34tSawn%V1IzzvLk*Km~ox|A8nhOW1Q{L>)YQxBssLcikv9OVla`Qk*C+vpb~A1 z)XmX`tB_0d<`HF4ku9UbCjZAQe(95747P2ZZmbBi{o*1Ut_ zGKa)sa^$o3LdHdox{^x~+~_H~FE@_Q@;>9s3*6Q{5W+LNZ~X1fNO7Ahehw;=u`uiV zj0_U-tn|#)6Oz9NzxDp*SUXawM@`V*NRegr56h)n#9LvhUR3H+UZv62H0I9-=HTA% zWIZ}xv-j_eZK$2JISE`MF#WVnxe?e1Y{1MySxwM>%SPq+ScW(Hm5Zr)6Go=7L`~cL zRv5>T#i8Nh6^Cf|H%I-x_t-w3c7Dp&`PrW+J>#81(%5dEz9#Zs3Qg5mQ7AGew=rJ- zfH6|Re@^t2!&FP_m-T-v4qGBdncN&cGml~NvtHZPPj99d3_>SD%zc zXl?JCmKyF*f7`0r7FrEo3a4HDF^#^$7`;J(z1nBa%=rU~qOUFr$@(roU-Hx$tL)bO zk%|o-CY7kZLil#)rQr;IT3^)9r~mM3Azv4M$RsvPn>EuZ5939!hen~VpCX+py{ei} zm4vW;U%Y(S{3^tVWADv0hIg35;UWK@?LiY@PcaR_ZKAsfg(V%(fmn^#-pD#zZ^jeY zF!k#vkp>XHFw%cgv?sO2T0rSh#UkTgdnc$kIL4hTWq=1YDlR@3^7k)tFnCrEwRrl1?|=ULEpM=#Jpnu zGDL4hI*cu8jg+C<@;AUY0$Drs4ioUQ<5vT?vig9J-3B*Hc}s3?x=j3m5^s}G0#AG% zdv0*D+FhG^QT~^hZUH-@rackM=NK{rz9@SAZ;rYTi9Kk#TAye<7uBc2HSduS*L#M{ z7dX*|PBt`lHf+Cz%nX?fdm3BP=#)%QF9aN13U0y+mpy0P#UM0pb$Yf=e@1=b!}OW5 z>pS4OhKAhpWw)Qm0VGHjetN-h8h=4$7TrVQ8sj2+p!8oQk+ zna*Elit|_zvV2VSjZ%+KKR@Nyt085*K;GB;<0YdEdW#j)LxInKu5gk`E6Rl#1PA7P zxghT~P+Et+qYFz@ zEnLldeJ_2*9iTBm(sq<^G5NoQzOb*Oi*osYwlbOLf=+?{-Tdu zRhlq(M}e;K`Ri@-He88@^5H)p7c^oVd-@OrXMJ{ps)b8uLqKF;3ypMqQO>$!;w;@H zcUSC56TooszIpVUTy`N3NdvD+9FVZg^z@Im^ z^5K`d7_f$nFa%4}~W5iraF}n5PO*}@9x|BohFinT87q?wmDvDr3E{xIG zYOUlAf9=!}GfF5p{Z8M{^pGp=!Bs(DnYL?5Mr*1}eQE!8A@Oiov`s;-^Hx#!I4K=i zb+g|ex+~|UU35E1y|QEP1@@%(_8Weg>i9%$ViJD)pJ0=3S5JQmFk6mDdI&@QvhQ!C zb+0`9PH$0~5~LbN)AV|ixYVoc>(Ho3=JG`T@YUGlhblg&)_vXP8eAzOfo=P87e$jkv}Uaul2lw1tkq!C6{j{@+&o|46#Zs3^Ox zJwr<)A)TT~NOw1af`Bwghjcd#-6`EjNP~2jq|)6T0}KPw&3yOsem_`?HSlBR%sJ=Y z``TB0_Zc7uJ(Tkm3Je{+nqm=-cMFS@`+p=GN?kvZ^50kW-x>c15iAEl*KgFAc6?r-13BTck9!^ zwRsGyws_{y@)F=K95%vMIMg=_LFRsY%Un4I^`W!jwOYi_XoN}7;vh-%$;Bq|+i(g4 zYsyGv&{A2ZDum*nGRQm+JMkYwy!Fa0ZvdFCJr(~|`~Qnwu>eR&+RgSL-KN%Q>;%QX9j**>4XrB&OwM^T1KSZO-{21y*s>`nJ`DkoWC?he6EkL)? zDq!bKyg*%m!Mxf@aR)en(6Zw->NU&hUL7&oC*2GQ*=BWv?y~skTsHVhJ&d%9%wIn* zuHHVe9^XEB%pOjHad3}R6UW$_4Ji2XjfQZG7E0qXKVA zD0Kg?PDnnlJfw3AZN;d(Z4$}>+zt%duYU*Kxg>tA5(3Z!3phKVuwyq4us>#{~uI#o(%&BJ`>$DrudB}Hk>u0?Y*IN&mG z=@&GCDBXK!BThW$kwWj;2%M1r%(7gL%1X8Y$t6xZSt1<@ZS+eNR*>dV;=h}Eh1=jn z9Uit-5p(u`ZkMD(S|b`vGk!7_^_8K$@x|BzaTKYm7bN2*hn0k1NZ+gRg{xw&bh=?_ zcAlZ}`Cqu%N1zntusR^*IPJ+R-O2*c`opL;p<&E zN_k$GOiVIW6k(|qW5DM%*}FZna$5dY3a~a?uZt^v;l#GNoXGe+y$&%o34WSC3xbd; zA6$`~*Zr~TIQUShi)7?9Niwm82*wbnh~IX!s!{dMvIV%WJ!{?`j|MIOIY zR84uT7AHL(0W)sw&&ibTRR-aP_N@dU+R%W6muf8tjsU+nLFy{@j>+by!Bb1)y=Wk- z$%A{=@cvS_@pJbAot4X>dEkKR9YB=b3cfL2IMVp%Uk1v|bK}#^_SQ8Lc6oj%UnX^{w8QekHj<;nDs*PBPgHv&muTQP_THu%`Q*WDvrz)M!CWB7Tl%oBo{R(B6^V zs4eJ@O&icKDpa~r!B5M04zS3+TjpYj%r65z1DKeqRZgZnM}EW9&0ZsqsOdge8jJtB zBNOiKd^++Zq!8?_A0jvM{LpOHNzrvamy#fXFr8g$_s5*Hug^z{Z+qoseEzbv;O?G6 z@!0;mH8_xTqQ&c(1Z@qwi0XbZU6Zx9(_ZsFlcLH>NP>U0gWCD4Z|vIbr(d)BZ95U| z?n@#1k|nmrMQpPldG|Lw*fHHtF72Ani(l2+TvB5?x=upAKY2Uc{NUhzO$t)*x#YMAiFAa?!21fpzA-fZVkU6R90unen zQ8A6Y2l2f#KR%KGk?H7I`Sg9BwO>P9Az}Z03MlUjs8(vX>Qz)r;9XX$HISBAMn!Y` zTu0_^xBY}`XN#&u#G{Uq%n#R~@&LF}n$#1;$kECxdNwM2AQZC#xlK7-dEQ-Et7@W+s`&pJS81ENh%|PWMkO1w}`Qdv1r= zzITv#L(AuCS6LWkjmU=v-j53nod5q!TRjqt4=#!gm~>` zw|}kGf@#c&5T6|~3Ii?!059Hj?{5ky;A`jK8yy*$hPVF(x~SlZkj=?z9ylX^*F9ND zT$Zo2lxrI?MrYy=K>Z~8?POv%>-z_BkD%{lVMU!37oOMd?(*MHLY(6d;hHt1zVWXL zcI$26LTjq(g|b+#EqhDsygMTzB5S?1aY+)J7fOcj8Iv z${qWExDs*;8&(KsW$hnNVW|pi=LG_ALHTmgsl)dpy~Hj^#81(2&jT#22=GV&eWe#H zc99V5O8uOk`@X?{!Ct_zU;OOi3KGo9Dm5NXL_#x0+aEt6TP3Yt}z#NIaNs^1cSbw$QrF{9fAZ{8p4H&M23C`&&xrsaF*v~BUFTd`kUIiXvH0YcB%>+`cCY>eM)_R-fe@$|fZ#yIs?vOr7vOxxSld>N25IVyf;)4oPI&bnzEa=H(#>^MN-F*qZFPc&wCN z+hs?!R;5gQp+G^Zr&Hz9L98|3J0(fJJ(3hImb?j0_}VRo2~aeDelX0xYK6k^4u4na zffT+}7;Y@T>9iLmvRw`_fIR<5+jCfrz{yiVL7BYY?$-Rc!w8~NdJKO9Bu$yZg^t(T z$QtYGRjVX5Ur;2Urj-;t`W|c7qsg|PCoGloa&d8wH2C#i!xy_u1g&Qs0y4)kQNxz( zlt>C4ANV9NW={d4ZG5*ia4$G|()@Q*-r2bLYKgy8MRoqI#7amis{X-=j=;V-E1yW z(B~F68(T)Y&!S@K=%BfyL9_@=kCO)PPOVNt3fjsg3qzg*|4HiX9~G{5@`{liZm0DkCz}|DWfMqC+5dct~NU1e&N&;kLSZj z|Kb~o0J>R9c~;5y2kYHw0eHg~r<_~&ObVq(C79r=b10@vjc-uY;j=kER6 z?Z1omh=0gPWm5A_5{|f8txm9+0-B*EkX|hixF}t!w;rZ z&SKA`cI=&%`VXq^Rfh4X&1={gf%}#@!6d=xTEqSEgx>6V@Ige@yhk~hxyHyI zS|sB3Ic0N|gX~g6U{1XaN$Eeyw6`7as{Z!SDVM|M2h{e97+e3~8mX9PgPf$(<~` zq(Yow95vVh=b~%Z}V>8n{s*_k`9j{XPM3dI6I0n6n#%-DcIe zrTcY4MaSQRUXLqiaM^GQs@|SA+XjvT!kJR_unKhdYF*T;S8ZFWK%jYg9qvi!-Wi0a zwcg?ooFB8?c*RZ}dUw|*UG8XkoNN7!*ZRP>&gRM6_R;wzEsCyz?-_Nj`RH%?a8p?J z(mix&|FmlW53qB&)a7-W>^7W-!PlE`ScW>|J{dyH_b5R?-S%o3yH(9lw6yY*R*XY^6BcI6u9Yp)Etu%SuvU3HKMMc%V+Sj|ixKJ^@A>rW>*!|o<<{VlhU_F%- zZwkbN3?I+FfLFJYDCIuBb${-fwTVHorkO;A+ml)oiw>+eFT!Hu#NKeA^9i(+h(X6T zCUyZvs-VSJ(58VO8Va5(8B4@%_d9Mwl&_3hK%xfbX1)OmG4a+-;siYezI?msZGs8} zJU&x37aHxz9c%=GBtY$&HNUsRX{1(u@E2^+h2J-=HMx?Jth=C_8530E&aR;@KCk$W zc0XyldT-Lr{vB^>(i8V`t`wI>;?--G(yjV<{Ky;G_ z>876xRSQL*=XXD*2k_Q(1~mw|1z?i55VqYaO37`aO9EG2y7;mC@%pdcVcA+5>-PD| z!EJf!!7hGm*}{Mu5$q}Uz)}-nAQcG6)rkQYu|8m=2aru=6n40+jE72fxtclq5{QwCwfuIm?4M21#vs-4>;#uSX|<3ahOfN+D$X>A&vQ!7+91kq zV8HnKyTSVO1{m}$qml3{nP$Z&n0rG}+3%t|L*6I8SAd7sW^7&E)Gly4e`|y%oO+?Zz7@ z7?L--LZ>mhnP=;QyFWg+KclPHn^*Msl`ND7TI1A%z`Ipn%hI$QroSKbYn9ghjnMz@;q&9&;Slh_ zSwM=rOQYnc%HHNi5qp?NbfPwLpJwjz(d5Xgw**1G14FK#?sEC}5m+V)+XILlI;C zcXuzq?82?Ys=sM8voYU+A^20`Q8B%D-ytWcuc-?eggK5EO9)`T@g78|*Jl*sEOUp7 zuWwszukT=lDwDR~#2FRcL)*_m3TCrPzIkymqaW8YR9n6!eO=!z4P1O&^H^+kMkPVO z$arp*@OEQp@IVSR1{7|12S_08t3d$?q>LQl!mr`Dd)5vX7Zw8l3@7NIfRlvrHi`ts zF=V2%bjmYNyI9C1M`=VZKIC4jWg-$0ZZSj?e(8Nx>vyQe3b`iRtw9jp0bw_L>`xRA z8;*tR-5k}s>duf`>TdD7kgr{c9FM~FK=h-9<{JITxt*`*v~<%mQ|C2lKemj+>Dda& zo8G6#Z{+!7$^zKO&Z|5I*k&;D&-(Afh#fo$MINz3#AFcyL@P?wx-s!PlmBZxmFfD# zuwExQ?~aBQs$SF4#fW41O3J0KHLa$}&ETg%7``ehVQn4*7wy}!d;-n+%$pfoR(%0H z9131p1bC?6=RH}Xs}4)+X|SwD-*vIl)eiAQd>vZu`9k$TDoxi=8{UTWicPhwX3hG2 zV-S4hY6EKR7A&3zrtMTVI9_jfF-!adN=6% z8-mze;_fG7<3i|5CArp(-zL2GxU6jjU9EX;rkyp5JqB|6HBh6v)JZIpcz9QS*f{>3>-F{gJv@YC>*_^$%u&B|0`3oz= zKUs^E0VMJIv7*HVu50`3PXdZ0XRDjqx;DNiVfhp%ggHlHVf?4C4@~##WG2$ebQI;6 z@evWxfWz6;LCa26#qP4cD?psOy_iw?++ZrqHHwg2P_f&}oXpctK>3Z?Oe zXimG)U(iBk+0*!w>gw|9^|fej(Hq~(^xPB0wfdQ-9H`e( zS(1vJt|}cLVqXhH>fU`>aGo)QmFe_~1JKCZa}53i0&4%0h@Vj_|L!AdM6H>00#Lv8 zcNH?9|FF312Bahs_`_52@9t}#&<)AMPs;{~+Jxa5IZu0%et>-~{gC6hQ@b>MK z>}#>wbi0t+X~Fx%?{?AD;{MysYDlN^H$>WK@dK|wqx5OL0=CzwZ>rMx~f2<<^bsIWL+vz-6g1i~ctTo196inT5wLX-3DzX>^4J zNMyyuOu`S8F(L@52M&4I-;{z&33>GFEOkZiwlm#OHTA$RES(mWB}3X_v=L7sy&v|b zD4b?>rvm+L7yogB^2?lF0RcbBA|cakoxMIj zwR=r^Q}4&(qTStobNG!p^Mi9Lcej7d8Ap)$SY*g8@mZ9XtI2;5W&K1O(dA^Y! zCKbhGWkfyP+e>#{yBZe1w*vJ@D6v@Q`N=hmB6-^K;`&F_stFe3BYw1 zDrFy{%s~N{F9 z)HL(IJ~)b+Q?p*HswAG!;Zp0J+!F#jCLCk%1IxQW7$C3z=4`R&WYnJ#9fCYW2N~Bd z+TTOYE<9^YBY?pOhmJPx7!#LF?LRBRMb=IUvE$TUxDMw4GyaTaj{t(9A5^-S5({Buebkuu}UvKu4)Aze!8)9 z@AoOz%Aga|@6)F)bMp0Oq<&Bsfo1OA)2QV{ZDZvPcl;}!o%cq*fX$qV4BHNmO9*2buS{e1Nl?=vh+r9M z!B~OT3BjAVIX|k8Wdt%N_$g{wMIn7%dulUqSqNFZJT3+j$#aEyh$?7&&>Ibfu=S0Mihd%bA{_63|U!mpw`TWAAevn{U2{Y1?(jKff|d^zjZNenmQr zCL}c3xumF8NFwHMU?QDnjUm3>)uKcZXom$FmTjI7b zx=~jFf$oKbSCOZ{HkIFkM+j5DBZ=Y5R;E#Upg83xxJ5nQdbmcFf7mW;%*-UGqX z(wqJ|2HTr-c542@2{?Az;ecI>8!cQ+6x!?v747aZKu$zr1n$fNI9*Q`1A&!W2CJs6 z;Pg*6_>u|n+oO<5&-X_%iTG0cvN=lUYr$@J37u-ld0U&Nd{p=(;43fG8~X z@8k_%y8hB$?92 zd|@jl`kXjTf4eM=+v_{UBrVy3$rJMSu8zQV88qR0BCe%{)R+5JjP1`7{{0VV2UbI7 zW!q&nX*dD7LL5yMe!n&e`>CfJy|}Y&75CB%;)1*`&i;7RkbU89sL?@&;x%n5TzFf^ z0*1tEDxxo_V}H27_;x>LU7jYH;T*0<-XC}< zMZ-vFS_Rq%k^)3^&;P8L-u@ew-*o(t?-?V|_SrcuOYG%aXGJx`pVuNbV@?h(T{NxQ zjEgHj@`3Ud0=uQ&y2{Ptz|xDt1){A;$TwFL)(Hu~Sf77A;+sGr8z4pwWCmk8g`GIFc? zmfg~-PEMUr3#cyqh&W=xdHelR1$oTm*eq3bV3zejvfRVF!6Oz-L#1<23(Kh?O+DK@OwymN6>h`a|Lb4IjL)ZaP<-{=f66tCCTB@=~kaTCB! zNXTD+q%z312Zzuj$!QKwYO|mURQ;Sv_#Z|-OA6Knf0|(%oLyF1qdhEpvS~L7jK8a{ zsgd{d69t^;IRI$D6EB=JBsu0IQC=&?9^k{!kWG0xKiB8?oCUkIT|siBO-_mB@$q_@ zgAH|oQW|IoCvcl{c~g-06mkY|ntjo{k9%iYxz~b>?tKJHRMn08tjH`ymJ#9R@xyOT zXm=t53v{5{Eh!Q{qg1(hdhf9z2~p?Pw#NKBF6eRkJ_b3y?1oojm80N0vJ=ekL~aJM z&a=LXrmI_fb<yEZacn2!MehVg^hi`N8bt=X>GD=Gc-9m-Ay z+JF8k+N3Svu;>oaV$O+d-5br-<+_{`82Cevn@P&(#rFQav)U7Uf)hl3V&cZCt;WS} zXu400bZ>|jza`2GZ;H&pTgMAvIE`RP5VR&L0B-H`hn2uUa0nNds^jVltEWvr?1n~@ z2P%lfc*8dgyzjXRaz!_DU4?#CW6Cc=l;m#b@k0BI5;;5_*u5(Zg5&w&QCD4u;)vde z8SzloPmb;jC(g_T4h!#(@1f!%H=;UU0y!_|kCAM*4ihxB%_vJ8tC?H90v(+Q+W53j zO_7o1N@(KcLW6mB8GgM8TJ&*^?Kws`8spSm!-VMH>tcb7o*d`Lq$A54G3aw;qM~HV z6$pPM6D4Omy(y>h#|Ozb!&{VCn27FQGfyNhkRDhOZiH`*FCQ&7=$H|c`ehK~YdoAQ z9q2!Cg2J+wT<|f&r|!2*Tv66%E3hW;;#uiN~%I)y7_I+us2-5{=Dd`*WBdRGezk!`E31i=i) z^<6kv;ejXi=7!n_9C21@2q+2MiqP~73zs3HXmGb^x@+H>TybQt`j~$<(4EQnYBunf z0c!U6xbiAMrffOwdH|9!kX8W;G^Te;Bep{QV9R9{2<9DLypkb`_>5O&35Ah_wSAmJ zds#+?6ylO!k1yl-T3KKMK1&G^j#mi%Lwc{5nW$=wDwvk-kLE6Z_4+8+3sd2Pq_rlA z`x8tx0Njd25)|4OfrFjD--`}HOE!3H6~)#*=K{Q@RXzI&7F{nKChJYnwXHF@?GeZz z+HA|%`m;_Cwh}5(YGx3pmLD;o5(o@cn{0DNZ9*L4{<9)tqv?om(+StnA>`>t+2|6h zL@cRH)w{8flGa&Jheh(QmRj|#)pSKQnbYW8w{(caAm-3wXN&RkBbxCkJ#`$C$o%cPkwkGHKD6cK+K0VYQ;!=6q|L>v+a1z(#r+LjcD0h? z7Y&uRKc?<8pJwc-S(z@mjam+||Gea&q|EwtX6+O|<~ce1dMRPN-|2*HqSGz8;G<8< z;30w7`EB-wyESbfJy(hlnlxcBvs}(~`s`22GF0L0(F>>?Ak;y5tN#TfV9DlZ#$od- zLbQMeN(6fI$Cb9m1Knoa&7Ok^KWfnAw?jKsisAiBi>Ix4qjz-w0-25s zA$}8R>|N<6)t@t|o<{E`yMxh=U6Y)<><=x5=#2pfsh^(RKxfgv?1kI05c?O`yokYX z)zd5{>%S_X5`MOG40lqfX?QLkUP#d6kL!&UzaPWKP%4u}UtSq2S(tlwgm>e@fMIgF zg+@hdJIwm58%<}1O5ubyZlY#RTiZbL?JYfNh?4y8yP%tSI2uR>6RV%*!f2U!dEVYH zp8ok8nVY+qBMm6-u`rH+d&4@W;)L7OnpMK`=o!8wthNP@B z`3qui1Y)hud97@Z}O(-i*6?@3P zfi|Svc=7DR*p8hXO$mebwdFmbtm*L~Qx0cvze=)iA$a$FrnxiGBYz7!1_}3DEdaFOTWha$!-uJCjh6&1A1X1I`Lv!i5 zXDn0KxlS`1w26x$@-Y$<<5Uq%4YczMmiqCE+mh<)!Y-_TUT?lhKu(9eH2mIy=#i7+Mgzs2h@&6=%=Eg@|urJnc!Ma()nQfBZ zj3FH?8ASi`#T|b(XwN2;8WeEIR0*QJj-@m06KNZ)tilbOH^BhMtO&`^sVHVe<~M$) zH76plMQdxDY}zNhY;K~r`N&xJ_7hK<%{SteaodIlq=&mv!M2Lgwm0(&;m<$i0>FnU zJ7K&(n^ZIlZDuS2z=g++D<*z^5m%Q%fj=C3kyQB{PV{F*&N)C~T|eIdy?gjvxDA=s z2NqT-hRQaO^q9yyp+}AkCTyvGpN|i%xJ!<*?0kqJ^v|5rN>ki=s?1zUB=}Uz0pkkI-AzF zm=MQ|Ccacze1&#b%$JP~v@WXxL~Qyk9^7Y_r+fh?nFjOItl10>TLif=A3;FN61AE}Y0%7o05FcJwN6?>rmAQlu z<1Mj_a8`9*CGd%Ix%i>u(I;K`s%K3UWOlVpt`SmdoST&%#2Q4Y7Z;zTq~f%R$yyet zKYt@)`(xkt*`(wi<>kBgjp#|WcJXH;-Yjj26Mc3satB|~ga2f6ys)G3L*dBxI+SM; zI*he(;ZN?&_owMY4g!%)+SH27MMGAic$ZacBR&^Y5GN zq~OoG%qGaH4Qz%dS#ot=dzenjWP8LXVB^s(ldY|@jV|(vnmZCuT$^RCR4;)%y^lU4 zh_aHGc$V^6(>raQ7FIkmVT`wOi1StEk)EmNFy2<$DuQH zwQNRYjvOHMjc-E*#pL9HV?bcsGrd0Yuz5hKCo%BJ#%UxloRk!gv04fjRdX4!Vmw=(}%*9H7 z+yerQBmZB25`L}an!IoohCOava;2>*ADgg}=e*BNzY|p2+Y9xcf`R~kATd#GFz9x2 zucv$5sNN#@#19=NH${b{iaNh~j zuc&#l9T%mu+-4c zuK}TlZ{)JAtFrX2P9erOgz;KADo;-&|4Rlq zn3~!rg3y5O{VS~-7=cWjHm9ul!KwKf{qlA>?RUeRZGUpPmww(TA(WDPF?!T-Z>xet z_|zfitx~Xw$W-U|DXuw{r3`szgfd8+=d%XJi6@O7cl64Wd+Ppv=7DeSv7(4k8@5|H>Ghy9YGS6^~mjpK4a?q zd>jEs-QpO~Xam=V-AdPv^XVK004Y9a_x*6Z;tU&$ST1;@|2e?XiL-~ajjvK3z$kK` zz1*C=_zTVPDs-%a>y}1?$!_mDoSZf8|!iV9vfhE z{HTybUh}_3qWCDa%?d)2AAHOrHVC?Nw(}bWOpGvEYuw96`)74~`ArCV-`HF-C1Tdv zz{y>0SN7k{w0aThihJUL5;F6`KMLo+CBiM-{W$VsZeulwOw8 z%GoCJV7V|rD-)2b3o?z@!n5Y*Ai*?#^r#^mvZlUo|IWpI`mawGvU2jA#JL6qj$Byk z08Rd(xI?}l{H>bS`Pc}Q=Tq1p$Ns0)%*?6lKjc-o^6>hJ->nLLcdHomhnxpgqDh1x ztu#me#qg132^*F~pp~dIU__3iq9e;r3@4()z8^&i_-sJ}GRkk9YaW%5Z+8S0FV>&P zYSN;~pzCE87fL?{U=&;8+AL1xQqKbPmmX4PF$kGdS=qsA$BXv7TmBxw-^J!kVsIW> zHsYyqT-)b0yI`N`SkD?IL({@Vns@*3Vxkn3vM!g&D${R`-^h}HKmtvxSC*#2YZ}5^ ztLeb-Kyr)$Ap!jKRLehqa}bZogowp-tu@7MQ;N2CejDB^)-;n!svQXC+~4 z1o}kd7xAh4Q7~KFbxC_yekWWhgrn^Ca^P?6umX#@#l(0HsbfKZp{i?jeGCH`abdWq zediKj9L^_R$#c^I)zq+Nw zkBPegPUCfFRU8-7(k~fnUN@1AMrP9YMB#d){jSTWqM~*moxi|k#l62k)fK)NSuRf_ zIkSQYbm@C1eu{MGjDv@JaQIX?fdhcjw^@eyXMKzj*EfaDKfqb`?Xs1UNxaz;v9#Zb zp&52C6b1J4V_Zh;XNC~NF=umV?1TAiUAm0$UKQWV61pPA{Yl2~ZCVF;NBstrY9`qm7jckOGmTWX{ z*WI_sZW#Jb{P`UyY(3Mu!C_gTvIz{WfOV1;7s! z(MwK8=XHW!<5}4}xrg)O0|$G_p{PA~T?LFCR8X?tzRPefN5409EonJT2aORnWAu1a zXr|H@U@dy|`~YBhl~28~Okyxwp3b{AxKW)Y0w<}`Y3JAnXR^!n-Etb6^w!y1qt%7( zKu2SOLEk-_MW+b^5IO*Cp!^4JtR~g{H$Nk+Y5)4^18vtrrZr9|fY_aGCY?g?HYEV> z1xV zECAvw$K-U-m3YSowjRMci&2@wf_ikj9N=t8r=|b4FFH;O#hHZ%S2nU1dJU_ou2K{x z2?8inl)0;`^E@&H`9x;?xy(R-!x3!yBAElzdn~`rRK%gO&A0j0NMcR zSd$TWv*gnW(vM2pnD+Zjx}m8SGtT=XcVr``?gRRXvu%}}$hNoC^1vrC$uB`KJ3K7) zIYb3XZ2c*rO*Y}1O&=x|#x;N~(DC69j6a;9f_5Jd-xuvcO;}bU>Q}k)OG{ATYJ*%@ zS3XVQ5x|ast}sXYMx4Kmc;FKX>D3-fBJbCk66wcht!%89<510q1BOjxRoiyBCcK$O zTG5^F4fc);Lxh=ZMH(}Z$0IXBF zkg2E^pLTaZrcB+(TpW6^%i~|mfAkzfn6gBTkrFbIuw|z7>ahfq-ry~X(JE11l^3O@ zM)`Hde(JjNp*Y&~T~ar1PdDKA=#~0-d(CUL(E_IMw?z%MYlo=}=Wwdm7Mg#Fk%EOQ zfj}vSh@Vb5ZFCbMF1J;*@2|NVLoPD#8XAIbt^&#Cf$-sS0s}KZQ!Mg>%mW;(W`6Qc zzmw+MppDM$usJrN`s?Do)FgjHvQSdfa`lh%D$eSEXCahNiK$hx^h;Bp`#38(?J8M`e$Uc8;>qPgz}iu)>Xlte<6`LQ z_8n8{EWnX@3Sm|f38Vi})6!72AWEtm&CL?q{JPm;=oeRooPh%dvK?xZf%Yh;l89|3`Q6fO<`LBQfwhcypLk++N!~ z(O^OZ2bkajloG`_LCxdlRI@FGF^wxVYj-|S-1=Y`-HJQ@8%}tEjN*uFWYgZdxf*3= z8e6bS(nofd6g||QM4fM_RC^yYS-FgijX-)Q9n#<4>Z`uDm$0kQ>W!yDc#>wF-ZHp9 z!#ESy6ghUarrRX{lw|Tcz04N&pHaK8T))g)*jilVVjdzNq5NnyXwYM{{o~Vk`xf1p zax!}wCBf@xLSg*~nDuZO1A|~&OWjPY#dKjqj@4^XMLn|GH|3WY_KSI)RznW@YCZ*Q zDwfMl??3LrO!Gjk#}TxN-xCA>ENp<*H{!?3nSt2`ts2+M#6*U$#%e0i8kT_#E?fv=#yxx4k|$!^l+E zViuR=#C7ezxjbG*C5fbU8C^6MHNAk24Xt*I1w7Ady4r2vrsl@Q)NvIekINX^vC$XG zGjLh3{v8*d(0;@CvbVwTiTui;D{_1+E5M2Xs0SMB2YK;=3$ zt_(DGEbl6&>*_hTmQRtg3rpPjy^pLDGjoD3>$r+6G8c;82|jx!1B2ExQqZn)&VGf9 z8UUuYAF+^n>}ed>S<9$=!cm*%P84$v(dO5&oNnAet)_YuvD|RG$sfXkj>&KNL8O7@ zZEJFnXJSA;Ebc=9-Aoy$2vl^!Wq`r5)YO`t*850esXFL=Ah3A?2$rZ@PdBzA)Kz5R zrU;at&p^UBlgK&K9=C&DJ1+94PauIh%L!S<%nETi15Y}>=c6ZFQCw*^ z^#5S{iqc~{p%3ohP#=^Gis^b(ih3o&%Yu%~Rz*FOlCquT6%>uur^xoF$EQ8z>2S{+ z!`y;yelGIAhRkJ1+3ee4z0VPB>TAupkcz1HpgS{?F}Zuv)2<|JRLW5n)T+2L zDQhX53lnGk1zr;{G_$$Ahg3QX>wE852{Y>c9%|Vo>u$1(9ipvhc^-6l$tC$(jFk;% zX=f0wVPFt)m&oZ&BnPqgG!Lg|WWxdE7SgGI$c0IIdLxlAL;Ivlhygq;HMI-|B|vcs zFiu5}smT2O9akT7hOaaTsB~5fG}}G`TeIL>mE=M+ZG;${Ky#ja1b@yw!qRTfsnFa@TUQ%X~vdZwWCGZn02HE&u3eMek5J@CS}yUD1CX{3w7w8t2zzeNsxz-~WN$Z>jCw z8~UBS|0DqCth1r;#V*&kB|#>UJ0ckOM#l>Ze;;nuIzYL9{P%7)iq`GQt0xlF6t{;;yH7u!4S+;HJZw^Cx8@0|k~U9aJ3It8m_K_w2AR5 zjn?D5$k^Z|XgHIDz)mR*Dccm-9$*l2w;yj*oEVxc7D)m8c4z8xUT<@Xv}p}UTl(pf z$MRndZ_uD%v-G$(ktd3NCr=#;FpJRR2Y=m-3vG}KA zHwz9X@I%0V$EP2H{o>E>Tr-|%_Sv|P3RDXH?$j>%`3p^ppE4?P3Xbd3JQv902rsd? zj6O$=cFL+TOzU}lLnCvFY_z`j_o>OL{-ZY(8#g_hYw-0|U@(?cRsN%h(Y^A+YLS6r z(L8N}2pzD}z--Mp{LJc=MsT^qPJSak67*w+Q%M{@XCuk~zN*OJ3d=KM&>c)e$B6KSBVmkK6DM&6P5W8Tok0=VQTj|DVdz zQ}0T^ln2^ZoYvI-7`5ZdzZ@5J)%kgR7nGL-q4dyyeJNQIDsGJeV*jyVT42@kieGad zq=%kslK_ZLRqN}X4!R&C?-&9N)#zA5E3jCvY*ARNMruT!2I<#4O73-ip6M*HR*-Dv zlF$4Y8i-UXBjZ2nTzY;EgG*|M@pMz1?}oDajKNQ0BBMbEhySX`>8~*FkZEqtdltis zr_mko7;x#1^X0+Osny(%>ROc3WVSCqSGBE<*0LC`HY*bNdtRQt~5m|6{8~P`k$jcMKhW12VQ>@rm{`j}7N6GJ^uif;`6Wm|zeFu2{^Bld9kzpmL%OTkPJ){;AkO3V~ z^?gP|-C<$;*Q^WhK|_&GSTkW~DFWi(04E;|AUJ}jPI3gZYjTVj8S0@Hf)?$V5X z`VCQGD?1jFtE2lQd*q%8-Lzw z%idci0AXuts{532kpGw#v`+=D&q{M_+SXlog+@PV~-2w=Yd<1f6d~&Nh zQ^Mu0RAT|fsz35j?35CtFk5;H;hmsTrx|P3Xf2U&Ki}-tvcSt4vs0OKN6_!~l7-}` zgwHinyqS4HZvtC4FVOEZjkT!zCx&%U{j~J#=_9g8iAHK0#r;)s+hA%TUoszSbk{;RESU zY1q|7nL*jL6XBOwx{M~FfM*I}EZIBgD@8t<2Hk~G*cTcoiQ^Kv8s76oRg0I6+V<(e zlWsd{SvgvrDCsqP_N*y{n zMKFj+1oH;T(HFx~l`yNiX0rVpvJLmH%X}J^n^e;hb@-ai7`@|o;K$X}OwjP&m2sST z&#v@-A>DnNcQZgz@!ywjhJRI%j+mZ&Qs%pu)AD?uIL)m5KX(oXmsA_XD^8^|fD^tb zO$`TQfJW&K{#I_Q=yu3!4Y+)-aD?G7)2woHj`)Ja!1ty%Uq=ahyV49|HOIhSQ zAZH>nyEJkaF|qo!oIM^@BGYp)Ga4Ng4EJF(SoY*GdIT@0vS4c2LChRo?7s!E@fcmU z<@>r=BTXY%hf7=IrMA5tN?39>RAhcz5{?77h5$FIIoWfeTNFz{EG25ioM>Q}Z)Ca~ z0yZP~3Je>>gGn|(l(55tHr;&bD2ABYi-B2XI9tON#VlcS&6y;FBga50mW0`7`-<@; zT`PCU_2xy~dqo?l3frRF#KvENnRmrz{?dwq-hvt>i&rz%qSQ8)h?C%yR<^` zhLhIz6FEv5M229~LGQ3fg0?rDm?2d^C!PMj6w_=%B>Dc;us(ov)I>+kkSR?JaRlj`Z?bxhWIy~P&P$pJ?UvHO z#Ev8Q3C=-0i``C8EfVSu98s(RsTpNZ{9Af1LzH4@&y;@+xIGHZEky~M=7!Y zZEQFBTw)TDCbV{}K+9U~G5ARc_FonbJO|h}n9v z5{7#**!iuB|2*l6hYFMBLsz~#P_oUy&$KxKvH;DbL9PD|MTp47S6;7vhj=#iFpW?$ zLcJ~CiIr-ZOeq_|!e~X}dB5MPz+dTI6SZD>kE(c$$i_)7BaQxX+`F-AN^Y3tlz_MJ zv(wjPItOXqZznk9a`>5yvB~x6O|9yq(g;M>`z#+CKYzrDdH8*@J^U*R@@++*D2aaV zu^tevDpDO523OSK!k{`8Vn>xg?7$V)8HFWL{&R6T4x8CQzm^@!Lt2ruTPNs_BAg{U zuaouMIu>J7eTFKUy=}p~i*c}3>n*qOz`cJjhK>RzJkj6Xa^oJ8QEBjL{Q*GzR=){_~K|SmYje0nScip+uF2(xcIF`yeoHEz*{*w zi$SP2kd8i@{~-SFB+O~+l|q*5oaWoCoht9!QxBAgwYq3 zTRUMf*^VL?I^Mo4<#?K%kkr%b)vWeR@+ae zqsDgJ%(?mWADSr7cMN$&QV0kBz4F=vgldoL80;58Oc-fvUR%ndIr7ziRuvINM9MNo znKZ3UFY>8A`{133Blx|l;)(n67tkUwWAbfobr4$^Fz*%?m>MkB8ZL&~O9sn(`VM}q zc^RevfYh?+Y{VC2;wljq44NK`%HqUTl)UW%wx24_?7T3uiMyx@&Do3CHXi&03xieH z=_z4?O6GeUQIBkwZ{8HsZ~Mi*<@t9Rb|155{SN=T!|z&bNx3OTIa^tw@Q~kH1=;^z zKbrFK2Wnjk{t+=M^=VdJcMzaEf$4V>-!pqNmeA7zF%{!XC!ovC)m*aE$75~@hEG^b zUMxa+qqu#P;acDWpa&{%QRQecP-kEZwT8TjDh&SZmx_^> zn03uk$ipO&anT)FVcWL@x{{M;!S~W(wxk+dsliihnq25X$K@#M8g32IK` zF1#8e)|(0NLzVGsM`$X4A?{aVN;1O^RCxQJS>WCtA9W~z+ert?b%bM*6GrW+Rt+(W zgOwUj1O4;=@UuqagstQOt2ViV`&@ci#EyAu0RpBkc`97=KL~9QMb@8Y#mnmzC^kj)_;-*gk^HL71~+neESaqBN98S(e`rNT3%&g+!C|mwR8G-q zs9{g=n|gA#J#{z^y1F6}>++Y6j>qLD?o_~Kj5l$P(#$}PVRSNdv{7W#tAzvsfA{cp zt63trTC_a|zmMyWLp~_cIqOlvc}UdRObDXLlvGs=i zVT56d^Y*+|`!_7ZOR_XfaUvo+o&Qj2wmbdve2i)4Jr0Ph2q^%C(7g#Z!j+Ll zpm40i#`F2NCNwl;-ise8#0QD~QOluyl>FMcv`5o;X2YHjN$ZsVT@Bn=j?S_{j=ch) z-j;!AY)gP+HyjQ41ehw@=Scaf3rj~14PS{!8T$LKG}FfCX=mC&G}Yj5I~3e}!)bGI zE-RJ3O-OMQzFYB3hoWVPnC>4R) zxl!J$Dz`Cuv8iq;gks9Fxx8;Q{{ovBVQ|c&9L|K>bpA3k^YDUExGY})Lui}p^KGA^ zA`$BH7xazMMhX~Wzyt!`Kq)XIzOQLNhv3W*3fA5ufxH(K=W&T|mxfA9J-LPBfRy&Q znd1b#y8~uU3LdTWtrF$Ov25M?#kjO%t0-CI82I1f9BE0d-ygOScb+(%*UdFvU??jo z1++fph0JMTUW&cBw1}AP)~!p96$^$*cTGi4{rxYX>lwcWU8;n!LFsg9R4B(^|B~6p zsou!sNo0sf11IuyC?hhhMnJFBsd61LC$#VM-qxi1SI^7`anWYh^Pfx=h#7Ron7*`g z6r!3QBRmMmnfVhpNz~wAMOkybD7rshRo6gMtv=@({ypVLm8bN>S#5pkdS z$45#gQ-5Ut+=BzWkFt_1oq>^Cj8Ky~%TtSS8AS@wqa-;g)ZhZiIke0AQ~%2YxCz5b zYB+#C=>Yhhq@*NBkdZI{z94|?yFv)v#S*tMI(9HL3oLM;9flI95b%t9lPbR%|4d1+ zPgf=1+_H?0AJF)2F z7Un|3A>y5Du<>>mez{n;^*C-L0)g!O5%$!uHzJezjrSJbtv7!`%5Hp|mG^MhZIen! z_?i>sxgF(jldh_ie8lA>W=we7+Kl$<x+F)*Kz&D6M??g|P} zMP?v+31wB9Ukct&QQID&QCui?BgE(Ty6EE<0)cdDEZE1jS-m=Hy;(JzOVGFlYTtp* z9~2cL&J1A5rC7&kO&p><3Dt6p-M&m|J?xT!8<_c)CVwyxf74;^0|W@_x8Tl2kM7t2Z-!ZJ5XkLWpIp zTz)y0{L*;zH74zIMod)f68CeJnK?rJtm{^pC)VRkY)@hg(p}$nE(r8;c@vtZgwGgk z_jzh+{b9yL$HV0On&{K9qJZ@<-l)`NwAb!0?mfv;5AdIofaMrq-gb!NcE91#T=_

rU45Eeu^Zmc4xayBWM41(jcC$-7o3GdNWJ$L>Mx><<}*rCzqfm z<1OdQfHE6VZv;(s2xctLSt%a;GJfp^g+^kOwSAh(4GhFpvmPK7hUN2yad?K5y;$(( z?>u>Bd-P|huiMDzVCvh7LF#YTKH@u%@fPa!KGH2FGDg&GV{2_ILSApbrTEP4;`yho zrU*H%&Hl#y?(Q8Se;pL)eCkq5;HFE#Sl{2<83D9Wz0IJUMzoeR^8I|O+DgaivE^>! z&r+LRPw<`LAY_wXSADa>(_K~Ewjg2}_9?BN-&&slcs$fI*Np6P zBR;uEAg*qY39P{7M(d2<8|K(onX~rR>rZ>tPi*cd-_W=I7fW-*Pe;$$cm8qZBJTb( zs%Mpz_T7=TV;dVhu4;5e$=b0M!tIU7xu5GihTX5Pw*y~oARv3b6f5AX)k6x~`R-xp z_^zwiEUvcC+om}Lbp=t!0oygfQaVao6G< z^pxsQJpC1*VCn%z>^N>k&=~!H9*lE4R?5i+s|UpE0+vmf?jG#^s+Hm^9!a^Sg{K(s zb0(CHrkQZaHi})dvuRRR2SZbZO!QUmY@(zvMBI}lif(B*Gw)MW1^RLqj-l8&;0-!+ z^>pG=)sCyPEJdGO8XtF&7BrS2pu?ukzHVM(43G(P1fg)xZWqT@LPPeT5bFg>c6c+& zTuzC%SwwHY3Ra`*wxw(yS{KXrmeh=(Mme_Dw&MHyIwSm9@;V1d>;Xu1lKdw-uj{rG z0|OpwJGu&fduY(c`q^1$uKpUhMQ<4#6^j>zS<*dh%cN0XhzD}mL^J+-_|V$$eX^PQ zar#49Emqmtb2tIP#k9YF^l5vVt8NgNPY{@3k8{xpqNgaHzMxLPcz&4@%u~}hCjmM? zq>v<|GKt-tot8bbI_+j5euW|=Oi#dk+6cnqDXRv>ocP`?6J%bDFL^O8E`T5PR>AhU zTwi9akN)&g4DJLTQrePNpVErNSgskZ7gDQ6@a(p#T zptY9FQf2(H7d;%$S?d)mz$7{?p+TW>4?0Q`;(KJNjxw4Y+g2ohjT}~1sh2WVpsYKH zDvjYWQb1>IzaPt%V(_$xZjuMX%V7ZbD!xJ-LDP8hI1OUggr+JP|M~s3Rw;`rfsi7o zv*qZ|`4NrqCv|%{uGTxAgd;8Q^QUTEP{88X`^JlIJlMrqg{&`;Pg$%LE&jZ?xOzIu z!6S+es-N0g>XEtRS2sWvJdOfF&Z8QR$JH49!E77t^9|pct6)YNI5*EPLP3n->&<^Y zu=YEwTqop>DAaqzi8;Q zt*RQPd$xXN3j<Fm96xD?&O5VQKYLw4zndKz za@MgSYKRQrWo!*9nyYF_i(g6A>=Q+FcD5=AM~JE^Qb`v#r}sBgX=W2tBOyv>k#CgOz60 zx7Dn;<&cG7$tz_0v_~FELwhzXS_*cbc?HEfYN2B$%ozXln5eU&$PU!`BTud%d@S#? z?zYpJJt({F`Nz*Q5z4Gc^e)xYQGIE^sqdM9H!`3^;+~1C4vW_fspYrcA|Du$)Rlf% zu{JpL>srazn#Cw9$|dd9&mRWYIF0)|sD>Wl_LPz;?C~kCp9g=*YjR;NI4{d3c}Ltw z)x{}c^8MpZtF_HpUqy-?rnw|5xv+1XN)WObZ~eS`09zPaUwSY;{F$F8B(bl5cq4{9 z-9*;|{(zwj$0=+#0LueWnwS7RmD zR*Pu=0+xJtDZS0+T(;Qtcka*XU59^p^$F!1j;V||vY*q1A#OtMDc$-ae*lTFl_}K2 z?*9Jqbei`=o(JHK8x1ybcg#*n=EsBW?fPqEOS?MXu+AcjEwNk%-^2NhcgnX??nq&O zR|x7N#zucH(%y3fL>76bK*^?ldffo5T3+wMtZL9(ia1BY&W6xcA0Ef zAXoW5H>tAOi=Z(#J?rwbPmV5Sr?3SD8+TzkM;3~Qt8Y}OfB0U?9aip|ou-q60Mcl# zLkpmqk-&#iH*~{ApusX4>Zfk4D?(OAE?_04NTkk+YJ)soXnut5eOW;qImC=|eA@kD zUB8;DkVcv}bsLtNs=mEfJg;ewtF$^3qw06b$HPP1{uehH%LH6K((qwx9EaP$S~s;( z2(dFQQO*Y-q%?R+G%PcDP1C+;6*UM)k_bPrrPx$g%xgN2IJ9LG&up&Nxi&u5#r&{4 zR{lGYsT6RQBPfqY0Gj!5yD_!ifdFcJv<6y0G-IK3v|+U@dxc*8BIS<;K7=)}a0D~V z>Hasbg#{XV18}j8n@@*#`^(xFGvTu^%;MOeC@C&$){QX}qg9$ESZ9QPYgx|@j8u7E zS?%CN_eRhFV#6{7-2J!;X>9M-dA;eKI9kJg0`9zz*Y|bBQbZQgMV}lN z^zzWj7Jh*LZpraJ-QEgC%0sa3)mGiboeVv9d<{a!|5c@`H*yq!8I6DKN6t%S_YF2F zXNziE3blDo=;5c?ylsT^H1wiwW=|S-7r7em5yHJ6y#DskyWIA%77D;f%B?+eZdjLU zo##uceQ(M~F`p>yd~rG9X(qRDxklWOj4S)7ZqFINO@au>xT3vJn_!~Zfch3<6b=jZ zz$)rs#yeIqGCGS`dfp%{%R$Ki3uuy87oa5bu9aMlpiz=>c>6sjMFNd@kRNmP$66S= z#mvw7UwKG1i>3V0VA9L3F@5fL9C^%NXm`xQl-I3z1ED!G%E~R@gQDq&Kc-Az-m~>- zcJrvtE`$#4Hd$C4iMvMgr_-qY*4>c1xdBbv7)EX0QY=hAyn6LVLg4Zgxh1xB z4xuOZ!R~2e^DB?NA8&;or@R(ac0|?4ZCQIDuHR+aQ7ymW60A{$lHgP;4!wOC^`%2EOn!VbdUx6`Ba-X)v zXuOpe2k?fYYu5AS9F41n>TD!Tr4YO!Q=7V*7k<0X6b}%(W~WY@(0jkf(9G5$hI>Bq z-1CsOk4Uu00K5o9l`fhX;Cldg|9Ke>^p?Od3p)}zaZI0Y7D+rkUCbJQ{LatepRQU4 z48(E>W}cs>@JB4wlVdylj%W+QM1X^5;j^8CQsv^z@!`Zu-9=Peo#fQhmV<|s$Lm;Wr3b6 z_(+x)_J!^tm>@EpucoIwixVnm2AbM9o&q>Q6dXC>G|drBBC;$D;(nU%7LojH}t^^xW~=93I2KNcyGt+ntsn-l8qcOu}?VHoHxc?3&ycn~V;* zn9tX_{rx_Z!|T=+q)M?^Cc%!c@;YXC)y)m+VZ%yN^dl7lSSQom~l7(tSw# z@&hXu?_om&;u`8UaN;T@)a?-!{XYjUehuX@+>9X>`_FBjRy3EN`fvbiS% z%i2I9+{LYs3+DYAYH@TpAUpE&;tb^{2!zt!hGjm=BpI0uKY#v|AcAra+WGu71ib9xI4|4`Rje3MJBHz3FZIbPrN|I1c=_4Lw zq5UuofP%4pt;ZazFM9f$p5)MlW}M8ndfQK3R%!)_>#$ObcK4?QLrE<@q9|5Kpw5Hl z<>K_iOx?{!{r>#s9Lu=sT*uZ%%5(x`F`-(RY%EdKQyJ@rx!TQ!i866#Ns>R>-WPzq z@z}bhPXVJs+|%|Sc)n-4n3E&B`edtgH|-j)2Wly%*A>lhr>Z~1*1UC}V|X|br~J^O z?FA%U_#i}Q`YXrnP>x=72AT%aVm>0`F!zSS244m))=VT@P>DKQ5n1Mz|86YE=#|_p*$)vvoRqg} zOoS~mITccuy4Oe&$Wy6i9@;O=&E53rbH$bOK<|am$+3G(i6Otv^nF`xfdYTxO%?{~<%pE7-_Phxx#CgI@VV`YbD z*Xj_qReJ>R86S7;P~#-e0R?gGp8c365-Lj5Gf}z17jUPq6CYSv35DCBXhy4pLSGCg>iJF8 z==$SUZe3TL*{76}jo&*sjw`3x9#WZ?uW+l)8ZD-n0k-v_6j8X@16u;ioQ$ehNi8k# zXgoU>0b5I15=a9; zI$Kv)r_t`i`@wDn_v||=Ou#Mo3`9NT@rq|Y>eP#!x%!^5T*Ozb4gIr|kFNolV?O|d zMM&tG(N;pq@{aXe#Tb%-)$u_Oq`mF?H;(Oi*@hp1`q|y#;V~b`RU_)YFj*WPljL2u z6L>$u7%gbc2O`bdCdymhx%~ZWtowz*jms5D`1xj6O7SLlEF}j!C`W}IyCxb3 z5iz0bPuQw)8)QWn1qBn9f1{C*o{nDdEWZ{T>UNqCA57FlHJYZ&xXLU^V76fs> z1efimE8NN!7lY<1gg&R37^ofWXa_dn^E4mkdX9cgn2(o+Wq>Gk`^- z)?9N)X{L^C)Ssr~Q=N}g>EBAQ&SP9%Jj$jT#a1P!>mSax@oV^9YeyHmjO(q0;7#j% z--iR$8Hi8EWL~fh3{(Z*Bpw}FYq3EgHsys#^uYD@)nM=)1^;Qg*B>aKzY8)K?Lf}t zt-l-sq!59Do9c??{rc^0lqw>Kkdr@P1EghdUXkc4&FA@1MXi7qc1~j0ryUO#t>?;E zbMv)iEF4%@)R@}^>4gTPGS1ge?AC(KBXJG=y%3DcQzpEqysT9MFyV0wE5^aB;r#hx zX0v~>d90g!4tFm4-B|by-{oi+@gA>f>&GG3nrAsL8yNtQs$Y@hQKQV#9f)y`4>tA& zbB_f0;vrA!3fTt7XWbP28*B62kc4*zqC~OjdlmafI&| z;w5vqScKb6dGmOVH~Ffr$#sB6-oI60I^D3&<$x=(M2*|A{O6sJC>!TJCPh;p_?JrW+4k!r_li}8{<1%?0tmg&0P zr=wL{gXm50ka$XrDa)efdIZbbeLH&mQ@S?Ld}9EGB`Z)B+4)t-8o$Nywv^yvz0P$Q ziJn35y}p3qJ7ylI+JV^Zm*h)7cj1*8Fae1cbL{%<7zJj_6i};rdI`O`Gm$L)O+iQq za9R#0d51=L80izMFUWtiyd1{zhTed)aO*D!0_#;SPdwaqmn|Njmyipo$*^8yHf1+4 zJ3c>;3q-(qZkfO8nG*WPzbWbBk+5p@CRh``B%7I_S(3)YGKemGWwH=3g09%8S~9hY zuP25myeT{&r9Mf5Ov@6wp{0?R%6OqKkW);QN$wY3$W2^n$FS&2I#H8?LQ25zG=PmOvN9UywnnAVO@QVNb<}a}=iF)XVK;wqB#T zvU4}Y z1PV@Nffl~pWaKYTf2Nv%b1A`z-4o6B6OAe{C8#o&4KMfDrtcxc&1Q8q!WQ5^lrm!E z=nO0kP&<^&D{pl6HoyfxAAZpCN3&${tbIm8S>iY!&(e;Zq*^>60B@xf&u!hzwqQa2 zu2Pk8m=@;{O9|ZENL8@9vSYA4__2kUyhu0UdRa5YTFg$u&`@XO>)KK^8b|D?8mquO z(q$Q*UPEV7d4{>ISyzsiyKr>(PAU38GXc}(rvF!4)$)RZ<#+coIx^}x%~)h=11j2Y z5u_$2h9Gs)G=bv`$ni-D8D(4FWk6>M)9r@k^1)^y;PI@3mW9VRoDH)BV>N4`+EM zzngpB;f#JAEfkHt9{HHY!|zymXUi37r93Bk8d@>hIl2huJeOxxL^rFAr!R$f-Ei9u zeM46G*MShSx2=CK?4B+{V~BOTyDl2I8w|`vabEVPIqFIc4|5n4@#B)rQpmZuLb$cv zAFbL+-&6)sRq!J$mgI#7f0Kr#Mil37iZtA0FxfiS<}r>q4)1%@(Cuwb46m_o z1C0gWlHIbAZzOsfxkM9h&wqml5vqMLB7&)CNAYqQ;5h*B5S&sWwEjRO2)Xq=2dj!W z{5|jCmf`x^Nv_Pd{AGEnbhHaaJgVtsl0N3-fY)2^|1g)4#X* ztLnXn(~!bvZr5!9dg0Go6QG~SE-ly+UhTi*&b)0fqiTZ0qH#m3>esPHsYQ%G((2=yO7Q0;OV{a~iE#PaO>pseWH;?bJ zG;T+=fbQa48nG?iHn-_3vqzP2OU=5gyQB34k@77|7yW;L-|M)wn$CEdiZkhH>mq=7 zjNN#zQ@yO8XKnmJ;@Pp zeEpfJkTsaY*}kX3t+hH2-+Z+EytbyUVzr{{;t}UVCsO?RAB=I{`qR_+TkzGOH22Z? zwS&dPJ8#=<{CsZ=$H5-1Po$RE9hJ%7TiGcTKERU!I$h90MIz}m=-E%s4uHZCtUD;x ztuks7k+4=aDJt?ge5II*EG7NbOk<5#eK&8Ud-zMsdj-_L*O3CC&Yss*_;KFYT%mVK z!_Lpmc-Xt|(>;!xjW4#Ut)5QC%0U6bx^*Bb&qJf<&E^#1r$0lAV%en%IpFF2WO8AK zXr{W8#^-z21*Pf0x-YhO%bD7MH!sO!+}0~zonn%2y!gJ`opTk=uy0(%Y5nWxcZ>$1 z7G%VaW|Z#%dhHEyhtw*;9$77&kI)Ex(zQk*_VOk`Cea6KN2d>d<3!z)#_HJu5H#bs z5lZkqQ-JlHG~H}OZ`XaPYurk{piKhidcgNktP85)G1R1Da}56c_+|oz3TR@8ky|cC zKf8aukHGXXImDE2gFVT&qQTM~Wqtm}Nt^^D7g=#x#sFhanY^$+pA*})Q6Q$8-haAs zb?od$(RE-S2Nn)qg_PVY&Mzu~kE|1-V6d{**cHcbBu3xTh zy8W0@tuRBA|B*j*)pB#-H3)_fCd#7wxUY@*GZXd99oQ`=y5NlpwjP3pTwHpX!B@4oK3`lQ< z?q^kLHcr*cUDfRTyomNdfx%#Oh{MS46>Fi0tI*5gSNXj0F?}AwX^GtkwCNYpW;`N-%BSlhz4?@1?-$|5&^a}!$~4IXnn-ge))&o*V2 zJ@YTsP>7u@xf-G2m4e7kD#*9WeT#V=_P$o^)XArd$%#bw!NMbC7@?=U|zM<0IplLD@rud|EIST3h3dVJ}pT5Cf)ghA(iED;O{ zKq}t>y2qf?hpz8-Wmx1w!2ZvHxZ~;X2ZVEOQboqG{-P!18P3NeLD0hN3dO1Eq0Fz8 z*;5pjjKC!GujCY2LF*mAAV3Zy9M%_^ETdnNWp)SUj_r;B=oZpy?_f1HON())u#zpMwzu?gqTdo3)&H7#mFHi^c&6L?AcV@RcVis1PWKVo;hSh zM~89r18FI74Ub+@L-rd0fer5KsXueO z<>#?NUiIUAr**&UQo!!M+~&RP3Zn)ppxuaBABtz31(>)(Dzo_DxR**;Qtj+8WBKo+ zo-Y2H>D+dzu(Fzni8MaTUa=}q#{h>xqxCz)hT3`4ajo;<2;z2)WK7rkOAVH`H9w#` zi<$D_-feJ3j@pa4oXO^;*0-35&H1t480%2#MEMmh6Hw-b5)(f{h#TDLE9U=jT#sn> z^BfYiE&%%`el$)bP11)U01H5+;plP~>HRL{(@uo_8ftR6atbyPDZsgG!cm}`=!qJ! z?!xT>A_$LDwYzPDy4vTcdV1Lv!c`8*{mqj)S0GD*)Z%+g$So|Qv^v5^l_&~Lf{!_B zJrt-hp#g{_))Gz_NqN_SsTo7wDJS+Bp-5+ng~i`756G{0!F+S-3y(2O3=T-zB(|2l zY|}CVrXYjGIqK!X(SllKo!Y}4w%pG-Q9%0DEBw+z1c-N5?zg<2Hpsb6&6;M`e-e$g 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 Q!~g&Q literal 0 HcmV?d00001 From ecfe558f4f52628f805cb698028c66b6545a05fd Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Thu, 29 Jun 2023 20:56:07 +0100 Subject: [PATCH 18/52] Restructure v3-sdk guides and add LP data guide Move quoting, trading, routing to trading subsection Add guide on positions, NFPM and position data fetching Update Liquidity guides --- .../v3/guides/liquidity/01-position-data.md | 276 ++++++++++++++++++ ...ing-position.md => 02-minting-position.md} | 147 ++++++++-- ...g-position.md => 03-modifying-position.md} | 165 +++++++++-- ...llecting-fees.md => 04-collecting-fees.md} | 52 +++- ...uidity.md => 05-swap-and-add-liquidity.md} | 125 ++++++-- .../sdk/v3/guides/{ => trading}/02-quoting.md | 0 .../sdk/v3/guides/{ => trading}/03-trading.md | 0 .../sdk/v3/guides/{ => trading}/04-routing.md | 0 docs/sdk/v3/guides/trading/_category_.json | 5 + 9 files changed, 702 insertions(+), 68 deletions(-) create mode 100644 docs/sdk/v3/guides/liquidity/01-position-data.md rename docs/sdk/v3/guides/liquidity/{01-minting-position.md => 02-minting-position.md} (56%) rename docs/sdk/v3/guides/liquidity/{02-modifying-position.md => 03-modifying-position.md} (54%) rename docs/sdk/v3/guides/liquidity/{03-collecting-fees.md => 04-collecting-fees.md} (73%) rename docs/sdk/v3/guides/liquidity/{04-swap-and-add-liquidity.md => 05-swap-and-add-liquidity.md} (63%) rename docs/sdk/v3/guides/{ => trading}/02-quoting.md (100%) rename docs/sdk/v3/guides/{ => trading}/03-trading.md (100%) rename docs/sdk/v3/guides/{ => trading}/04-routing.md (100%) create mode 100644 docs/sdk/v3/guides/trading/_category_.json diff --git a/docs/sdk/v3/guides/liquidity/01-position-data.md b/docs/sdk/v3/guides/liquidity/01-position-data.md new file mode 100644 index 0000000000..ebf5c8d59b --- /dev/null +++ b/docs/sdk/v3/guides/liquidity/01-position-data.md @@ -0,0 +1,276 @@ +--- +id: position-data +title: Liquidity Positions +--- + +## Introduction + +This guide will introduce us to liquidity positions in Uniswap V3 and present the `v3-sdk` classes and Contracts used to interact with the protocol. +The concepts and code snippets showcased here can be found across the **Pooling Liquidity** examples in the Uniswap code examples [repository](https://github.com/Uniswap/examples). + +In this guide, we will take a look at the [Position](../../reference/classes/Position.md) and [NonfungiblePositionManager](../../reference/classes/NonfungiblePositionManager.md) classes, as well as the [NonfungiblePositionManager Contract](../../../../contracts/v3/reference/periphery/NonfungiblePositionManager.md). + +At the end of the guide, we should be familiar with the most important classes used to interact with liquidity positions. +We should also understand how to fetch and create positions on the Contract side. + +For this guide, the following Uniswap packages are used: + +- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) +- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) +- [`@uniswap/v3-periphery`](https://www.npmjs.com/package/@uniswap/v3-periphery) + +The code mentioned in this guide can be found across the [minting Position](https://github.com/Uniswap/examples/blob/main/v3-sdk/minting-position/src), [collecting Fees](https://github.com/Uniswap/examples/blob/main/v3-sdk/collecting-fees/src), [modifying positions](https://github.com/Uniswap/examples/blob/d34a53412dbf905802da2249391788a225719bb8/v3-sdk/modifying-position/src) and [swap and add liquidity](https://github.com/Uniswap/examples/blob/main/v3-sdk/swap-and-add-liquidity/src) examples. + +## Position class + +The **sdk** provides a `Position` class used to create local representations of an onchain position. +It is used to create the calldata for onchain calls to mint or modify an onchain position. + +There are four ways to construct a position. + +Directly with the [constructor](https://github.com/Uniswap/v3-sdk/blob/08a7c05/src/entities/position.ts#L40): + +```typescript +import { Pool, Position } from '@uniswap/v3-sdk' +import JSBI from 'jsbi' + +const pool = new Pool(...) +const tickLower: number = -100 +const tickUpper: number = 200 +const liquidity: JSBI = JSBI.BigInt('1000000000000000000') + +const position = new Position({ + pool, + liquidity, + tickLower, + tickUpper +}) +``` + +Using the [`fromAmounts()`](https://github.com/Uniswap/v3-sdk/blob/08a7c05/src/entities/position.ts#L312) function: + +```typescript +import { BigIntish } from '@uniswap/sdk-core' + +const pool = new Pool(...) +const tickLower: number = -100 +const tickUpper: number = 200 +const amount0: BigIntish = '1000000000000000000' +const amount1: BigIntish = JSBI.BigInt('1000000000000000000') +const useFullPrecision: boolean = true + +const position = Position.fromAmounts({ + pool, + tickLower, + tickUpper, + amount0, + amount1, + useFullPrecision +}) +``` + +Or using the [`fromAmount0()`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/entities/position.ts#L354) or [`fromAmount1()`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/entities/position.ts#L378) functions: + +```typescript +import { BigIntish } from '@uniswap/sdk-core' +... + +const pool = new Pool(...) +const tickLower: number = -200 +const tickUpper: number = 100 +const amount0: BigIntish = '1000000000000000000' +const useFullPrecision: boolean = true + +const singleSidePositionToken0 = Position.fromAmount0({ + pool, + tickLower, + tickUpper, + amount0, + useFullPrecision +}) + +const amount1: BigIntish = 100000000 + +const singleSidePositionToken1 = Position.fromAmount0({ + pool, + tickLower, + tickUpper, + amount1, + useFullPrecision +}) +``` + +These last two functions calculate a position at the given tick range given the amount of `token0` or `token1` and an unlimited amount of the other Token. +For example, if a tick range where the ratio between `token0` and `token1` is 1 : 2 is defined by `tickLower` and `tickUpper`, the position would be created with that ratio. +A create transaction would then fail if the wallet doesn't hold enough `token1` or the Contract is not given the necessary **Transfer Approval**. + +All of these functions take an Object with **named values** as a call parameter. The amount and liquidity values are of type `BigIntish` which accepts `number`, `string` and `JSBI`. + +## NonfungiblePositionManager + +The `NonfungiblePositionManager` class is mainly used to create calldata for functions on the **NonfungiblePositionManager Contract**. + +We will look at the **sdk** class and write functions on the Contract in this section. + +### Creating a Position + +To create a position on a Pool, the [`mint`](../../../../contracts/v3/reference/periphery/NonfungiblePositionManager.md#mint) function is called on the Contract. +The **sdk** class provides the `addCallParameters` function to create the calldata for the transaction: + +```typescript +import { MintOptions, NonfungiblePositionManager } from '@uniswap/v3-sdk' + +const mintOptions: MintOptions = { + recipient: address, + deadline: Math.floor(Date.now() / 1000) + 60 * 20, + slippageTolerance: new Percent(50, 10_000), +} + +// get calldata for minting a position +const { calldata, value } = NonfungiblePositionManager.addCallParameters( + positionToMint, + mintOptions +) +``` + +This call creates a position if it doesn't exist, but can also be used to increase an existing position. +Take a look at the [Mint Position guide](./02-minting-position.md) and [Modify Position guide](./04-modifying-position.md) to learn more. + +### Decreasing and Increasing a Position + +To decrease or increase the liquidity of a Position, the `decreaseLiquidity` or `increaseLiquidity` functions are called on the Contract. +To increase, `addCallParameters` is used as mentioned above, to decrease we use `removeCallParameters`: + +```typescript +const { calldata, value } = NonfungiblePositionManager.removeCallParameters( + currentPosition, + removeLiquidityOptions +) +``` + +Take a look at the [Modify Positions guide](04-modifying-position.md) to learn how to create the `currentPosition` and `removeLiquidityOptions` parameters. + +## Fetching Positions + +The [NonfungiblePositionManager Contract](../../../../contracts/v3/reference/periphery/NonfungiblePositionManager.md) can be used to create Positions, as well as get information on existing Positions. + +In this section we will fetch all positions for an address. + +### Creating an ethers Contract + +We use **ethersJS** to interact with the NonfungiblePositionManager Contract. Let's create an ethers Contract: + +```typescript +import { ethers } from 'ethers' +import INONFUNGIBLE_POSITION_MANAGER from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json' + +const provider = new ethers.providers.JsonRpcProvider(rpcUrl) + +const nfpmContract = new ethers.Contract( + NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, + INONFUNGIBLE_POSITION_MANAGER.abi, + provider +) +``` + +We get the Contract ABI from the 'v3-periphery` package and the contract address from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md) + +### Fetching the Position Ids + +We want to fetch all Position Ids for our address. We first fetch the number of positions and then the ids by their indices. + +We fetch the number of positions using the `balanceOf` read call: + +```typescript + +const numPositions = await nfpmContract.balanceOf(address) +``` + +Next we iterate over the number of positions and fetch the ids: + +```typescript +const calls = [] + +for (let i = 0; i < numPositions; i++) { + calls.push( + nfpmContract.tokenOfOwnerByIndex(address, i) + ) +} + +const positionIds = await Promise.all(calls) +``` + +### Fetching the Position Info + +Now that we have the ids of the Positions associated with our address, we can fetch the position info using the `positions` function. + +The solidity function returns a lot of values describing the Position: + +```solidity +function positions( + uint256 tokenId + ) external view returns ( + uint96 nonce, + address operator, + address token0, + address token1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ) +``` + +In this example we only care about values needed to interact with positions, so we create an Interface `PositionInfo`: + +```typescript +interface PositionInfo { + tickLower: number + tickUpper: number + liquidity: JSBI + feeGrowthInside0LastX128: JSBI + feeGrowthInside1LastX128: JSBI + tokensOwed0: JSBI + tokensOwed1: JSBI +} +``` + +We fetch the Position data with `positions`: + +```typescript +const positionCalls = [] + +for (let id of positionIds) { + positionCalls.push( + nfpmContract.positions(id) + ) +} + +const callResponses = await Promise.all(positionCalls) +``` + +Finally, we map the RPC response to our interface: + +```typescript +const positionInfos = callResponses.map((position) => { + return { + tickLower: position.tickLower, + tickUpper: position.tickUpper, + liquidity: JSBI.BigInt(position.liquidity), + feeGrowthInside0LastX128: JSBI.BigInt(position.feeGrowthInside0LastX128), + feeGrowthInside1LastX128: JSBI.BigInt(position.feeGrowthInside1LastX128), + tokensOwed0: JSBI.BigInt(position.tokensOwed0), + tokensOwed1: JSBI.BigInt(position.tokensOwed1), + } +}) +``` + +We now have an array containing PositionInfo for all positions that our address holds. + +## Next steps + +Now that you are familiar with the most important classes and Contract to interact with Liquidity Positions, continue with the next guide on [Minting Positions](./02-minting-position.md). diff --git a/docs/sdk/v3/guides/liquidity/01-minting-position.md b/docs/sdk/v3/guides/liquidity/02-minting-position.md similarity index 56% rename from docs/sdk/v3/guides/liquidity/01-minting-position.md rename to docs/sdk/v3/guides/liquidity/02-minting-position.md index 81b46c68aa..89e69d10eb 100644 --- a/docs/sdk/v3/guides/liquidity/01-minting-position.md +++ b/docs/sdk/v3/guides/liquidity/02-minting-position.md @@ -36,44 +36,123 @@ The core code of this guide can be found in [`mintPosition()`](https://github.co The first step is to give approval to the protocol's `NonfungiblePositionManager` to transfer our tokens: -```typescript reference title="Approving our tokens for transferring" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/minting-position/src/libs/positions.ts#L46-L51 +```typescript +const token0Approval = await getTokenTransferApproval( + token0Address, + amount0 +) +const token1Approval = await getTokenTransferApproval( + token1Address, + amount1 +) ``` The logic to achieve that is wrapped in the `getTokenTransferApprovals` function. In short, since both **USDC** and **DAI** are ERC20 tokens, we setup a reference to their smart contracts and call the `approve` function: -```typescript reference title="Setting up an ERC20 contract reference and approving" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/minting-position/src/libs/positions.ts#L202-L211 +```typescript +import { ethers, BigNumber } from 'ethers' + +async function getTokenTransferApproval(address: string, amount: BigNumber) { + const provider = new ethers.providers.JsonRpcProvider(rpcUrl) + + const tokenContract = new ethers.Contract( + token.address, + ERC20_ABI, + provider + ) + + return tokenContract.approve( + NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, + amount + ) +} ``` +We can get the Contract address for the NonfungiblePositionManager from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). + ## Creating an instance of a `Pool` Having approved the transfer of our tokens, we now need to get data about the pool for which we will provide liquidity, in order to instantiate a Pool class. -To start, we compute our Pool's address by using a helper function and passing in the unique identifiers of a Pool - the **two tokens** and the Pool **fee**. The **fee** input parameter represents the swap fee that is distributed to all in range liquidity at the time of the swap: +To start, we compute our Pool's address by using a helper function and passing in the unique identifiers of a Pool - the **two tokens** and the Pool **fee**. +The **fee** input parameter represents the swap fee that is distributed to all in range liquidity at the time of the swap: -```typescript reference title="Computing the Pool's address" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/minting-position/src/libs/pool.ts#L24-L29 +```typescript +import { computePoolAddress, FeeAmount } from '@uniswap/v3-sdk' +import { Token } from '@uniswap/sdk-core' + +const token0: Token = ... +const token1: Token = ... +const fee: FeeAmount = ... +const POOL_FACTORY_CONTRACT_ADDRESS: string = ... + +const currentPoolAddress = computePoolAddress({ + factoryAddress: POOL_FACTORY_CONTRACT_ADDRESS, + tokenA: token0, + tokenB: token1, + fee: poolFee, +}) ``` +Again, we can get the factory contract address from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). + Then, we get the Pool's data by creating a reference to the Pool's smart contract and accessing its methods: -```typescript reference title="Setting up a Pool contract reference and fetching current state data" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/minting-position/src/libs/pool.ts#L31-L45 +```typescript +import IUniswapV3PoolABI from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json' + +const poolContract = new ethers.Contract( + currentPoolAddress, + IUniswapV3PoolABI.abi, + provider +) + +const [liquidity, slot0] = + await Promise.all([ + poolContract.liquidity(), + poolContract.slot0(), + ]) ``` Having collected the required data, we can now create an instance of the `Pool` class: -```typescript reference title="Fetching pool data and creating an instance of the Pool class" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/minting-position/src/libs/positions.ts#L111-L118 +```typescript +import { Pool } from '@uniswap/v3-sdk' + +const configuredPool = new Pool( + token0, + token1, + poolFee, + slot0.sqrtPriceX96.toString(), + liquidity.toString(), + slot0.tick +) ``` ## Calculating our `Position` from our input tokens Having created the instance of the `Pool` class, we can now use that to create an instance of a `Position` class, which represents the price range for a specific pool that LPs choose to provide in: -```typescript reference title="Create a Position representation instance" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/minting-position/src/libs/positions.ts#L121-L132 +```typescript +import { Position } from '@uniswap/v3-sdk' +import { BigIntish } from '@uniswap/sdk-core' + +// The maximum token amounts we want to provide. BigIntish accepts number, string or JSBI +const amount0: BigIntish = ... +const amount1: BigIntish = ... + +const position = Position.fromAmounts({ + pool: configuredPool, + tickLower: + nearestUsableTick(configuredPool.tickCurrent, configuredPool.tickSpacing) - + configuredPool.tickSpacing * 2, + tickUpper: + nearestUsableTick(configuredPool.tick, configuredPool.tickSpacing) + + configuredPool.tickSpacing * 2, + amount0: amount0, + amount1: amount1, + useFullPrecision: true, +}) ``` We use the `fromAmounts` static function of the `Position` class to create an instance of it, which uses the following parameters: @@ -87,18 +166,50 @@ Given those parameters, `fromAmounts` will attempt to calculate the maximum amou The Position instance is then passed as input to the `NonfungiblePositionManager`'s `addCallParameters` function. The function also requires an [`AddLiquidityOptions`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/nonfungiblePositionManager.ts#L77) object as its second parameter. This is either of type [`MintOptions`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/nonfungiblePositionManager.ts#L74) for minting a new position or [`IncreaseOptions`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/nonfungiblePositionManager.ts#L75) for adding liquidity to an existing position. For this example, we're using a `MintOptions` to create our position. -```typescript reference title="Getting the transaction calldata and parameters" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/minting-position/src/libs/positions.ts#L78-L88 +```typescript +import { MintOptions, NonfungiblePositionManager } from '@uniswap/v3-sdk' +import { Percent } from '@uniswap/sdk-core' + +const mintOptions: MintOptions = { + recipient: address, + deadline: Math.floor(Date.now() / 1000) + 60 * 20, + slippageTolerance: new Percent(50, 10_000), +} + +// get calldata for minting a position +const { calldata, value } = NonfungiblePositionManager.addCallParameters( + position, + mintOptions +) ``` The function returns the calldata as well as the value required to execute the transaction: -```typescript reference title="Submitting the Position NFT minting transaction" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/minting-position/src/libs/positions.ts#L91-L100 +```typescript +const transaction = { + data: calldata, + to: NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, + value: value, + from: address, + maxFeePerGas: MAX_FEE_PER_GAS, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS, +} ``` +We use our wallet to send the transaction. As it is a write call, we need to sign the transaction with a valid private key. + +```typescript +const wallet = new ethers.Wallet(privateKey, provider) + +const txRes = await wallet.sendTransaction(transaction) +``` + +Write calls do not return the result of the transaction. If we want to read the result we would need to use for example `trace_transaction`. +You can find an example of that in the [Range Order guide](../advanced/05-range-orders.md). +In this example, we don't need the result of the transaction. + The effect of the transaction is to mint a new Position NFT. We should see a new position with liquidity in our list of positions. ## Next Steps -Once you have minted a position, our next guide ([Adding and Removing Liquidity](./02-modifying-position.md)) will demonstrate how you can add and remove liquidity from that minted position! +Once you have minted a position, our next guide ([Adding and Removing Liquidity](./03-modifying-position.md)) will demonstrate how you can add and remove liquidity from that minted position! diff --git a/docs/sdk/v3/guides/liquidity/02-modifying-position.md b/docs/sdk/v3/guides/liquidity/03-modifying-position.md similarity index 54% rename from docs/sdk/v3/guides/liquidity/02-modifying-position.md rename to docs/sdk/v3/guides/liquidity/03-modifying-position.md index ecde86d717..017a6198fb 100644 --- a/docs/sdk/v3/guides/liquidity/02-modifying-position.md +++ b/docs/sdk/v3/guides/liquidity/03-modifying-position.md @@ -28,7 +28,7 @@ For this guide, the following Uniswap packages are used: The core code of this guide can be found in [`addLiquidity()`](https://github.com/Uniswap/examples/blob/d34a53412dbf905802da2249391788a225719bb8/v3-sdk/modifying-position/src/example/Example.tsx#L33) and [`removeLiquidity()`](https://github.com/Uniswap/examples/blob/733d586070afe2c8cceb35d557a77eac7a19a656/v3-sdk/modifying-position/src/example/Example.tsx#L83) :::note -This guide assumes you are familiar with our [Minting a Position](./01-minting-position.md) guide. A minted position is required to add or remove liquidity from, so the buttons will be disabled until a position is minted. +This guide assumes you are familiar with our [Minting a Position](./02-minting-position.md) guide. A minted position is required to add or remove liquidity from, so the buttons will be disabled until a position is minted. Also note that we do not need to give approval to the `NonfungiblePositionManager` to transfer our tokens as we will have already done that when minting our position. ::: @@ -37,32 +37,106 @@ Also note that we do not need to give approval to the `NonfungiblePositionManage Assuming we have already minted a position, our first step is to construct the modified position using our original position to calculate the amount by which we want to increase our current position: -```typescript reference title="Creating the Position" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/modifying-position/src/libs/liquidity.ts#L46-L61 +```typescript +const fractionToAdd: number = ... + +const amount0Increased: JSBI = fromReadableAmount( + readableAmount0 * fractionToAdd, + token0.decimals +) +const amount1Increase: JSBI = fromReadableAmount( + readableAmount1 * fractionToAdd, + token1.decimals +) + +const positionToIncreaseBy = constructPosition( + amount0Increased, + amount1Increase + ) +) ``` -The function receives two arguments, which are the `CurrencyAmount`s that are used to construct the Position instance. In this example, both of the arguments follow the same logic: we multiply the parameterized `tokenAmount` by the parameterized `fractionToAdd` since the new liquidity position will be added on top of the already minted liquidity position. +The `fromReadableAmount()` function calculates the amount of tokens in their smallest unit, so for example 1 ETH would be `1000000000000000000` Wei as ETH has 18 decimals. + +A better way to get the amounts might be to fetch them with the positionId directly from the blockchain. +We demonstrated how to do that in the [first guide](./01-position-data.md#fetching-positions) of this series. + +```typescript +import { Pool, Position } from '@uniswap/v3-sdk' +import JSBI from 'jsbi' + +function constructPosition( + amount0: JSBI, + amount1: JSBI +): Position { + // create Pool same as in the previous guide + const pool = new Pool(...) + + // create position using the maximum liquidity from input amounts + return Position.fromAmounts({ + pool, + tickLower: + nearestUsableTick(pool.tickCurrent, pool.tickSpacing) - + pool.tickSpacing * 2, + tickUpper: + nearestUsableTick(pool.tickCurrent, pool.tickSpacing) + + pool.tickSpacing * 2, + amount0, + amount1, + useFullPrecision: true, + }) +} +``` + +The function receives two arguments, which are the amounts that are used to construct the Position instance. In this example, both of the arguments follow the same logic: we multiply the parameterized `tokenAmount` by the parameterized `fractionToAdd` since the new liquidity position will be added on top of the already minted liquidity position. We then need to construct an options object of type [`AddLiquidityOptions`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/nonfungiblePositionManager.ts#L77) similar to how we did in the minting case. In this case, we will use [`IncreaseOptions`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/nonfungiblePositionManager.ts#L75): -```typescript reference title="Constructing the options object" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/modifying-position/src/libs/liquidity.ts#L63-L67 +```typescript +import { AddLiquidityOptions } from '@uniswap/v3-sdk' + +const addLiquidityOptions: AddLiquidityOptions = { + deadline: Math.floor(Date.now() / 1000) + 60 * 20, + slippageTolerance: new Percent(50, 10_000), + tokenId, +} ``` Compared to minting, we have we have omitted the `recipient` parameter and instead passed in the `tokenId` of the position we previously minted. +The tokenId can be fetched with the tokenOfOwnerByIndex function of the NonfungiblePositionManager Contract as described [here](./01-position-data.md#fetching-positions). The newly created position along with the options object are then passed to the `NonfungiblePositionManager`'s `addCallParameters`: -```typescript reference title="Passing the position and options object to addCallParameters" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/modifying-position/src/libs/liquidity.ts#L70-L73 +```typescript +import { NonfungiblePositionManager } from '@uniswap/v3-sdk' + +const { calldata, value } = NonfungiblePositionManager.addCallParameters( + positionToIncreaseBy, + addLiquidityOptions +) ``` The return values of `addCallParameters` are the calldata and value of the transaction we need to submit to increase our position's liquidity. We can now build and execute the transaction: -```typescript reference title="Building and submitting the transaction" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/modifying-position/src/libs/liquidity.ts#L76-L85 +```typescript +import { ethers } from 'ethers' + +const transaction = { + data: calldata, + to: NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, + value: value, + from: address, + maxFeePerGas: MAX_FEE_PER_GAS, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS, +} + +const wallet = new ethers.Wallet(privateKey, provider) + +const txRes = await wallet.sendTransaction(transaction) ``` +We can get the Contract address for the NonfungiblePositionManager from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). + After pressing the button, note how the balance of USDC and DAI drops and our position's liquidity increases. ## Removing liquidity from our position @@ -71,41 +145,88 @@ The `removeLiquidity` function is the mirror action of adding liquidity and will To start, we create a position identical to the one we minted: -```typescript reference title="Creating an identical position as minting" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/modifying-position/src/libs/liquidity.ts#L97-L112 +```typescript +const amount0: JSBI = fromReadableAmount( + readableAmount0 * fractionToAdd, + token0.decimals +) +const amount1: JSBI = fromReadableAmount( + readableAmount1 * fractionToAdd, + token1.decimals +) + +const currentPosition = constructPosition( + amount0, + amount1 +) ``` We then need to construct an options object of type [`RemoveLiquidityOptions`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/nonfungiblePositionManager.ts#L138): -```typescript reference title="Constructing the options object" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/modifying-position/src/libs/liquidity.ts#L126-L133 +```typescript +import { RemoveLiquidityOptions } from '@uniswap/v3-sdk' +import { Percent } from '@uniswap/sdk-core' + +const removeLiquidityOptions: RemoveLiquidityOptions = { + deadline: Math.floor(Date.now() / 1000) + 60 * 20, + slippageTolerance: new Percent(50, 10_000), + tokenId: positionId, + // percentage of liquidity to remove + liquidityPercentage: new Percent(0.5), + collectOptions, +} ``` Just as with adding liquidity, we have we have omitted the `recipient` parameter and instead passed in the `tokenId` of the position we previously minted. We have also provide two additional parameters: -- `liquidityPercentage` determines how much liquidity is removed from our initial position (as a `Percentage`), and transfers the removed liquidity back to our address. We set this percentage from our guide configuration ranging from 0 (0%) to 1 (100%). +- `liquidityPercentage` determines how much liquidity is removed from our initial position (as a `Percentage`), and transfers the removed liquidity back to our address. We set this percentage from our guide configuration ranging from 0 (0%) to 1 (100%). In this example we would remove 50% of the liquidity. - [`collectOptions`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/nonfungiblePositionManager.ts#L105) gives us the option to collect the fees, if any, that we have accrued for this position. In this example, we won't collect any fees, so we provide zero values. If you'd like to see how to collect fees without modifying your position, check out our [collecting fees](./03-collecting-fees.md) guide! -```typescript reference title="Constructing the collect options object" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/modifying-position/src/libs/liquidity.ts#L114-L124 +```typescript +import { CurrencyAmount } from '@uniswap/sdk-core' +import { CollectOptions } from '@uniswap/v3-sdk' + +const collectOptions: Omit = { + expectedCurrencyOwed0: CurrencyAmount.fromRawAmount( + token0, + 0 + ), + expectedCurrencyOwed1: CurrencyAmount.fromRawAmount( + token1, + 0 + ), + recipient: address, +} ``` The position object along with the options object is passed to the `NonfungiblePositionManager`'s `removeCallParameters`, similar to how we did in the adding liquidity case: -```typescript reference title="Getting the calldata and value for the transaction" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/modifying-position/src/libs/liquidity.ts#L135-L138 +```typescript +const { calldata, value } = NonfungiblePositionManager.removeCallParameters( + currentPosition, + removeLiquidityOptions +) ``` The return values `removeCallParameters` are the calldata and value that are needed to construct the transaction to remove liquidity from our position. We can build the transaction and send it for execution: -```typescript reference title="Building and submitting the transaction" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/modifying-position/src/libs/liquidity.ts#L141-L150 +```typescript +const transaction = { + data: calldata, + to: NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, + value: value, + from: address, + maxFeePerGas: MAX_FEE_PER_GAS, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS, +} + +const txRes = await wallet.sendTransaction(transaction) ``` After pressing the button, note how the balance of USDC and DAI increases and our position's liquidity drops. ## Next Steps -Now that you can mint and modify a position, check out how to [collect fees](./03-collecting-fees.md) from the position! +Now that you can mint and modify a position, check out how to [collect fees](./04-collecting-fees.md) from the position! diff --git a/docs/sdk/v3/guides/liquidity/03-collecting-fees.md b/docs/sdk/v3/guides/liquidity/04-collecting-fees.md similarity index 73% rename from docs/sdk/v3/guides/liquidity/03-collecting-fees.md rename to docs/sdk/v3/guides/liquidity/04-collecting-fees.md index 67480bc40a..06ac92b732 100644 --- a/docs/sdk/v3/guides/liquidity/03-collecting-fees.md +++ b/docs/sdk/v3/guides/liquidity/04-collecting-fees.md @@ -37,12 +37,38 @@ Also note that we do not need to give approval to the `NonfungiblePositionManage All of the fee collecting logic can be found in the [`collectFees`](https://github.com/Uniswap/examples/blob/be67e7df220b0a270c9d18bbaab529e017213adf/v3-sdk/collecting-fees/src/example/Example.tsx#L24) function. Notice how the **Collect Fees** button is disabled until a position is minted. This happens because there will be no fees to collect unless there is a position whose liquidity has been traded against. -To start, we construct an options object of type [`CollectOptions`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/nonfungiblePositionManager.ts#L105) that holds the data about the fees we want to collect: +To start, we fetch the position from the NonfungiblePositionManager Contract to get the fees we are owed: -```typescript reference title="Constructing the CollectOptions" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/collecting-fees/src/libs/liquidity.ts#L44-L61 +```typescript +import { ethers } from 'ethers' +import JSBI from 'jsbi' +... + +const nfpmContract = new ethers.Contract(NONFUNGIBLE_POSITION_MANAGER_ADDRESS, provider) +const position = nfpmContract.positions(positionId) +``` + +Next, we construct an options object of type [`CollectOptions`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/nonfungiblePositionManager.ts#L105) that holds the data about the fees we want to collect: + +```typescript +import { CurrencyAmount } from '@uniswap/sdk-core' + +const collectOptions: CollectOptions = { + tokenId: positionId, + expectedCurrencyOwed0: CurrencyAmount.fromRawAmount( + CurrentConfig.tokens.token0, + JSBI.BigInt(position.tokensOwed0) + ), + expectedCurrencyOwed1: CurrencyAmount.fromRawAmount( + CurrentConfig.tokens.token1, + JSBI.BigInt(position.tokensOwed1) + ), + recipient: address, +} ``` +Read more about fetching position info [here](./01-position-data.md#fetching-positions). + Similar to the other functions exposed by the `NonfungiblePositionManager`, we pass the `tokenId` and the `recipient` of the fees, which in this case is our function's input position id and our wallet's address. The other two `CurrencyAmount` parameters (`expectedCurrencyOwed0` and `expectedCurrencyOwed1`) define the **maximum** amount of currency we expect to get collect through accrued fees of each token in the pool. We set these through our guide's configuration. @@ -51,18 +77,28 @@ The other two `CurrencyAmount` parameters (`expectedCurrencyOwed0` and `expected We then get the call parameters for collecting our fees from our `NonfungiblePositionManager` using the constructed `CollectOptions`: -```typescript reference title="Getting the calldata and value for the transaction" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/collecting-fees/src/libs/liquidity.ts#L64-L65 +```typescript +const { calldata, value } = + NonfungiblePositionManager.collectCallParameters(collectOptions) ``` The function above returns the calldata and value required to construct the transaction for collecting accrued fees. Now that we have both the calldata and value we needed for the transaction, we can build and execute the it: -```typescript reference title="Building and submitting the transaction" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/collecting-fees/src/libs/liquidity.ts#L68-L77 +```typescript +const transaction = { + data: calldata, + to: NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, + value: value, + from: address, + maxFeePerGas: MAX_FEE_PER_GAS, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS, +} + +const txRes = await wallet.sendTransaction(transaction) ``` After pressing the button, if someone has traded against our position, we should be able to note how the balance of USDC and DAI increases as we collect fees. ## Next Steps -The previous guides detail all the atomic steps needed to create and manage positions. However, these approaches may not use all of your desired currency. To ensure you are using your full funds while minimizing gas prices, check out our guide on [Swapping and Adding Liquidity](./04-swap-and-add-liquidity.md) in a single transaction! +The previous guides detail all the atomic steps needed to create and manage positions. However, these approaches may not use all of your desired currency. To ensure you are using your full funds while minimizing gas prices, check out our guide on [Swapping and Adding Liquidity](./05-swap-and-add-liquidity.md) in a single transaction! diff --git a/docs/sdk/v3/guides/liquidity/04-swap-and-add-liquidity.md b/docs/sdk/v3/guides/liquidity/05-swap-and-add-liquidity.md similarity index 63% rename from docs/sdk/v3/guides/liquidity/04-swap-and-add-liquidity.md rename to docs/sdk/v3/guides/liquidity/05-swap-and-add-liquidity.md index fdac7171f1..be1b7d20d6 100644 --- a/docs/sdk/v3/guides/liquidity/04-swap-and-add-liquidity.md +++ b/docs/sdk/v3/guides/liquidity/05-swap-and-add-liquidity.md @@ -42,17 +42,32 @@ Also note that we do not need to give approval to the `NonfungiblePositionManage The first step is to approve the `SwapRouter` smart contract to spend our tokens for us in order for us to add liquidity to our position: -```typescript reference title="Approve SwapRouter to spend our tokens" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/ec48bb845402419fa6e613cb26512a76d864afa5/v3-sdk/swap-and-add-liquidity/src/libs/liquidity.ts#L58-L66 +```typescript +const tokenInApproval = await getTokenTransferApproval( + token0, + V3_SWAP_ROUTER_ADDRESS +) + +const tokenOutApproval = await getTokenTransferApproval( + token1, + V3_SWAP_ROUTER_ADDRESS +) ``` -The we can setup our router, the [`AlphaRouter`](https://github.com/Uniswap/smart-order-router/blob/97c1bb7cb64b22ebf3509acda8de60c0445cf250/src/routers/alpha-router/alpha-router.ts#L333), which is part of the [smart-order-router package](https://www.npmjs.com/package/@uniswap/smart-order-router). The router requires a `chainId` and a `provider` to be initialized. Note that routing is not supported for local forks, so we will use a mainnet provider even when swapping on a local fork: +We described the `getTokenTransferApproval` function [here](./02-minting-position.md#giving-approval-to-transfer-our-tokens). -```typescript reference title="Creating a router instance" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/swap-and-add-liquidity/src/libs/liquidity.ts#L57 +Then we can setup our router, the [`AlphaRouter`](https://github.com/Uniswap/smart-order-router/blob/97c1bb7cb64b22ebf3509acda8de60c0445cf250/src/routers/alpha-router/alpha-router.ts#L333), which is part of the [smart-order-router package](https://www.npmjs.com/package/@uniswap/smart-order-router). The router requires a `chainId` and a `provider` to be initialized. Note that routing is not supported for local forks, so we will use a mainnet provider even when swapping on a local fork: + +```typescript +import { ethers } from 'ethers' +import { AlphaRouter } from '@uniswap/smart-order-router' + +const provider = new ethers.providers.JsonRpcProvider(rpcUrl) + +const router = new AlphaRouter({ chainId: 1, provider }) ``` -For a more detailed example, check out our [routing guide](../04-routing.md). +For a more detailed example, check out our [routing guide](../trading/03-routing.md). ## Configuring our ratio calculation @@ -60,14 +75,41 @@ Having created the router, we now need to construct the parameters required to m The first two parameters are the currency amounts we use as input to the `routeToRatio` algorithm: -```typescript reference title="Constructing the two CurrencyAmounts" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/c4667fadb13584268bbee2e0e0f556558a474751/v3-sdk/swap-and-add-liquidity/src/libs/liquidity.ts#L78-L92 +```typescript +import { CurrencyAmount } from '@uniswap/sdk-core' + +const token0CurrencyAmount = CurrencyAmount.fromRawAmount( + token0, + fromReadableAmount( + token0AmountToAdd, + token0.decimals + ) +) + +const token1CurrencyAmount = CurrencyAmount.fromRawAmount( + token1, + fromReadableAmount( + token1AmountToAdd, + token1.decimals + ) +) ``` Next, we will create a placeholder position with a liquidity of `1` since liquidity is still unknown and will be set inside the call to `routeToRatio`: -```typescript reference title="Constructing the position object" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/swap-and-add-liquidity/src/libs/liquidity.ts#L75-L78 +```typescript +import { Pool, Position, nearestUsableTick } from '@uniswap/v3-sdk' + +const placeholderPosition = new Position{ + pool, + liquidity: 1, + tickLower: + nearestUsableTick(pool.tickCurrent, pool.tickSpacing) - + pool.tickSpacing * 2, + tickUpper: + nearestUsableTick(pool.tickCurrent, pool.tickSpacing) + + poolInfo.tickSpacing * 2 +} ``` We then need to create an instance of `SwapAndAddConfig` which will set additional configuration parameters for the `routeToRatio` algorithm: @@ -75,8 +117,14 @@ We then need to create an instance of `SwapAndAddConfig` which will set addition - `ratioErrorTolerance` determines the margin of error the resulting ratio can have from the optimal ratio. - `maxIterations` determines the maximum times the algorithm will iterate to find a ratio within error tolerance. If max iterations is exceeded, an error is returned. The benefit of running the algorithm more times is that we have more chances to find a route, but more iterations will longer to execute. We've used a default of 6 in our example. -```typescript reference title="Constructing SwapAndAddConfig" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/swap-and-add-liquidity/src/libs/liquidity.ts#L80-L83 +```typescript +import { Fraction } from '@uniswap/sdk-core' +import { SwapAndAddConfig } from '@uniswap/smart-order-router' + +const swapAndAddConfig: SwapAndAddConfig = { + ratioErrorTolerance: new Fraction(1, 100), + maxIterations: 6, +} ``` Finally, we will create an instance of `SwapAndAddOptions` to configure which position we are adding liquidity to and our defined swapping parameters in two different objects: @@ -84,22 +132,49 @@ Finally, we will create an instance of `SwapAndAddOptions` to configure which po - **`swapConfig`** configures the `recipient` of leftover dust from swap, `slippageTolerance` and a `deadline` for the swap. - **`addLiquidityOptions`** must contain a `tokenId` to add to an existing position -```typescript reference title="Constructing SwapAndAddOptions" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/c4667fadb13584268bbee2e0e0f556558a474751/v3-sdk/swap-and-add-liquidity/src/libs/liquidity.ts#L104-L114 +```typescript +import { SwapAndAddOptions } from '@uniswap/smart-order-router' + +const swapAndAddOptions: SwapAndAddOptions = { + swapOptions: { + type: SwapType.SWAP_ROUTER_02, + recipient: address, + slippageTolerance: new Percent(50, 10_000), + deadline: Math.floor(Date.now() / 1000) + 60 * 20, + }, + addLiquidityOptions: { + tokenId: positionId, + }, +} ``` ## Calculating our currency ratio Having constructed all the parameters we need to call `routeToRatio`, we can now make the call to the function: -```typescript reference title="Making the call to routeToRatio" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/swap-and-add-liquidity/src/libs/liquidity.ts#L97-L103 +```typescript +import { SwapToRatioResponse } from '@uniswap/smart-order-router' + +const routeToRatioResponse: SwapToRatioResponse = await router.routeToRatio( + token0CurrencyAmount, + token1CurrencyAmount, + currentPosition, + swapAndAddConfig, + swapAndAddOptions +) ``` The return type of the function call is [SwapToRatioResponse](https://github.com/Uniswap/smart-order-router/blob/97c1bb7cb64b22ebf3509acda8de60c0445cf250/src/routers/router.ts#L121). If a route was found successfully, this object will have two fields: the status (success) and the `SwapToRatioRoute` object. We check to make sure that both of those conditions hold true before we construct and submit the transaction: -```typescript reference title="Checking that a route was found" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/swap-and-add-liquidity/src/libs/liquidity.ts#L105-L110 +```typescript +import { SwapToRatioStatus } from '@uniswap/smart-order-router' + +if ( + !routeToRatioResponse || + routeToRatioResponse.status !== SwapToRatioStatus.SUCCESS +) { + // Handle Failed Transaction +} ``` In case a route was not found, we return from the function a `Failed` state for the transaction. @@ -108,8 +183,18 @@ In case a route was not found, we return from the function a `Failed` state for After making sure that a route was successfully found, we can now construct and send the transaction. The response (`SwapToRatioRoute`) will have the properties we need to construct our transaction object: -```typescript reference title="Constructing and sending the transaction" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/swap-and-add-liquidity/src/libs/liquidity.ts#L112-L120 +```typescript +import { SwapToRatioRoute } from '@uniswap/smart-order-router' + +const route: SwapToRatioRoute = routeToRatioResponse.result +const transaction = { + data: route.methodParameters?.calldata, + to: V3_SWAP_ROUTER_ADDRESS, + value: route.methodParameters?.value, + from: address, +} + +const txRes = await wallet.sendTransaction(transaction) ``` If the transaction was successful, our swap-and-add will be completed! We should see our input token balances decrease and our position balance should be increased accordingly. diff --git a/docs/sdk/v3/guides/02-quoting.md b/docs/sdk/v3/guides/trading/02-quoting.md similarity index 100% rename from docs/sdk/v3/guides/02-quoting.md rename to docs/sdk/v3/guides/trading/02-quoting.md diff --git a/docs/sdk/v3/guides/03-trading.md b/docs/sdk/v3/guides/trading/03-trading.md similarity index 100% rename from docs/sdk/v3/guides/03-trading.md rename to docs/sdk/v3/guides/trading/03-trading.md diff --git a/docs/sdk/v3/guides/04-routing.md b/docs/sdk/v3/guides/trading/04-routing.md similarity index 100% rename from docs/sdk/v3/guides/04-routing.md rename to docs/sdk/v3/guides/trading/04-routing.md diff --git a/docs/sdk/v3/guides/trading/_category_.json b/docs/sdk/v3/guides/trading/_category_.json new file mode 100644 index 0000000000..c60d7589d6 --- /dev/null +++ b/docs/sdk/v3/guides/trading/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Trading", + "position": 2, + "collapsed": false +} From 67bd34f0e0c5568eca90597568626d3d8423cf8c Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Fri, 30 Jun 2023 12:31:45 +0100 Subject: [PATCH 19/52] Move trading guides to folder, update all trading guides --- docs/sdk/v3/guides/01-background.md | 3 +- docs/sdk/v3/guides/advanced/_category_.json | 2 +- docs/sdk/v3/guides/liquidity/_category_.json | 2 +- .../02-quoting.md => swaps/01-quoting.md} | 120 +++++++++++-- .../03-trading.md => swaps/02-trading.md} | 150 ++++++++++++---- docs/sdk/v3/guides/swaps/03-routing.md | 162 ++++++++++++++++++ docs/sdk/v3/guides/swaps/_category_.json | 5 + docs/sdk/v3/guides/trading/04-routing.md | 74 -------- docs/sdk/v3/guides/trading/_category_.json | 5 - 9 files changed, 393 insertions(+), 130 deletions(-) rename docs/sdk/v3/guides/{trading/02-quoting.md => swaps/01-quoting.md} (66%) rename docs/sdk/v3/guides/{trading/03-trading.md => swaps/02-trading.md} (50%) create mode 100644 docs/sdk/v3/guides/swaps/03-routing.md create mode 100644 docs/sdk/v3/guides/swaps/_category_.json delete mode 100644 docs/sdk/v3/guides/trading/04-routing.md delete mode 100644 docs/sdk/v3/guides/trading/_category_.json diff --git a/docs/sdk/v3/guides/01-background.md b/docs/sdk/v3/guides/01-background.md index 7270fe21a1..f2108d3caa 100644 --- a/docs/sdk/v3/guides/01-background.md +++ b/docs/sdk/v3/guides/01-background.md @@ -1,12 +1,13 @@ --- id: background title: Background +position: 1 --- Before integrating with Uniswap, it may be helpful for newcomers to review the following background information on some important developer web3 concepts, the structure of our examples, and SDK concepts. :::info -Already familiar with web3 development and/or the basics of our SDK and want to get right to the code? Start with our first guide, [Getting a Quote](./02-quoting.md)! +Already familiar with web3 development and/or the basics of our SDK and want to get right to the code? Start with our first guide, [Getting a Quote](./trading/01-quoting.md)! ::: ## Providers diff --git a/docs/sdk/v3/guides/advanced/_category_.json b/docs/sdk/v3/guides/advanced/_category_.json index 65c1e31617..e190f9f051 100644 --- a/docs/sdk/v3/guides/advanced/_category_.json +++ b/docs/sdk/v3/guides/advanced/_category_.json @@ -1,5 +1,5 @@ { "label": "Advanced", - "position": 6, + "position": 4, "collapsed": true } diff --git a/docs/sdk/v3/guides/liquidity/_category_.json b/docs/sdk/v3/guides/liquidity/_category_.json index 1a66cbbe2c..b7bfa33814 100644 --- a/docs/sdk/v3/guides/liquidity/_category_.json +++ b/docs/sdk/v3/guides/liquidity/_category_.json @@ -1,5 +1,5 @@ { "label": "Pooling Liquidity", - "position": 5, + "position": 3, "collapsed": true } diff --git a/docs/sdk/v3/guides/trading/02-quoting.md b/docs/sdk/v3/guides/swaps/01-quoting.md similarity index 66% rename from docs/sdk/v3/guides/trading/02-quoting.md rename to docs/sdk/v3/guides/swaps/01-quoting.md index bd6db1b3ef..d5494efcab 100644 --- a/docs/sdk/v3/guides/trading/02-quoting.md +++ b/docs/sdk/v3/guides/swaps/01-quoting.md @@ -31,46 +31,111 @@ For this guide, the following Uniswap packages are used: The core code of this guide can be found in [`quote.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/quoting/src/libs/quote.ts) +## Example configuration + +We will use the example configuration `CurrentConfig` in most code snippets of this guide. It has the format: + +```typescript +import { Token } from '@uniswap/sdk-core' + +interface ExampleConfig { + rpc: { + local: string + mainnet: string + } + tokens: { + in: Token + amountIn: number + out: Token + poolFee: number + } +} + +export CurrentConfig: ExampleConfig = { + ... +} +``` + ## Computing the Pool's deployment address To interact with the **USDC - WETH** Pool contract, we first need to compute its deployment address. The SDK provides a utility method for that: -```typescript reference title="Computing the Pool's address" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/quoting/src/libs/quote.ts#L40-L45 +```typescript +const currentPoolAddress = computePoolAddress({ + factoryAddress: POOL_FACTORY_CONTRACT_ADDRESS, + tokenA: CurrentConfig.tokens.in, + tokenB: CurrentConfig.tokens.out, + fee: CurrentConfig.tokens.poolFee, +}) ``` Since each *Uniswap V3 Pool* is uniquely identified by 3 characteristics (token in, token out, fee), we use those in combination with the address of the *PoolFactory* contract to compute the address of the **USDC - ETH** Pool. These parameters have already been defined in our configuration file: -```typescript reference title="Configuration Parameters" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/1ef393c2b8f8206a3dc5a42562382c267bcc361b/v3-sdk/quoting/src/config.ts#L34-L39 +```typescript +const WETH_TOKEN = new Token( + 1, + '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + 18, + 'WETH', + 'Wrapped Ether' +) + +const USDC_TOKEN = new Token( + 1, + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + 6, + 'USDC', + 'USD//C' +) + +tokens: { + in: USDC_TOKEN, + amountIn: 1000, + out: WETH_TOKEN, + fee: FeeAmount.MEDIUM, +}, ``` +We can find the Pool Factory Contract address for our chain [here](../../../../contracts/v3/reference/Deployments.md). + ## Referencing the Pool contract and fetching metadata Now that we have the deployment address of the **USDC - ETH** Pool, we can construct an instance of an **ethers** `Contract` to interact with it: -```typescript reference title="Setting up a reference to the Pool contract" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/quoting/src/libs/quote.ts#L47-L51 +```typescript +import { ethers } from 'ethers' + +const provider = new ethers.providers.JsonRpcProvider(rpcUrl) +const poolContract = new ethers.Contract( + currentPoolAddress, + IUniswapV3PoolABI.abi, + provider +) ``` To construct the *Contract* we need to provide the address of the contract, its ABI and the provider that will carry out the RPC call for us. We get access to the contract's ABI through the [@uniswap/v3-core](https://www.npmjs.com/package/@uniswap/v3-core) package, which holds the core smart contracts of the Uniswap V3 protocol: -```typescript reference title="Uniswap V3 Pool smart contract ABI" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/quoting/src/libs/quote.ts#L5 +```typescript +import IUniswapV3PoolABI from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json' ``` Having constructed our reference to the contract, we can now access its methods through our provider. -We use a batch `Promise` call. This approach queries state data concurrently, rather than sequentially, to avoid out of sync data that may be returned if sequential queries are executed over the span of two blocks: - -```typescript reference title="Getting Pool metadata from the Pool smart contract" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/quoting/src/libs/quote.ts#L52-L56 +We use a batch `Promise` call. This approach queries state data concurrently, rather than sequentially, to minimize the chance of fetching out of sync data that may be returned if sequential queries are executed over the span of two blocks: + +```typescript +const [token0, token1, fee] = await Promise.all([ + poolContract.token0(), + poolContract.token1(), + poolContract.fee(), +]) ``` The return values of these methods will become inputs to the quote fetching function. +The `token0` and `token1` variables are the addresses of the tokens in the Pool and should not be mistaken for `Token` objects from the sdk. :::note In this example, the metadata we fetch is already present in our inputs. This guide fetches this information first in order to show how to fetch any metadata, which will be expanded on in future guides. @@ -80,16 +145,22 @@ In this example, the metadata we fetch is already present in our inputs. This gu Like we did for the Pool contract, we need to construct an instance of an **ethers** `Contract` for our Quoter contract in order to interact with it: -```typescript reference title="Setting up a reference to the Quoter contract" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/quoting/src/libs/quote.ts#L14-L18 +```typescript +const quoterContract = new ethers.Contract( + QUOTER_CONTRACT_ADDRESS, + Quoter.abi, + getProvider() +) ``` We get access to the contract's ABI through the [@uniswap/v3-periphery](https://www.npmjs.com/package/@uniswap/v3-periphery) package, which holds the periphery smart contracts of the Uniswap V3 protocol: -```typescript reference title="Uniswap V3 Quoter smart contract ABI" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/b5e64e3d6c17cb91bc081f1ed17581bbf22024bc/v3-sdk/quoting/src/libs/quote.ts#L4 +```typescript +import Quoter from '@uniswap/v3-periphery/artifacts/contracts/lens/Quoter.sol/Quoter.json' ``` +We get the QUOTE_CONTRACT_ADDRESS for our chain from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). + We can now use our Quoter contract to obtain the quote. In an ideal world, the quoter functions would be `view` functions, which would make them very easy to query on-chain with minimal gas costs. However, the Uniswap V3 Quoter contracts rely on state-changing calls designed to be reverted to return the desired data. This means calling the quoter will be very expensive and should not be called on-chain. @@ -97,10 +168,21 @@ In an ideal world, the quoter functions would be `view` functions, which would m To get around this difficulty, we can use the `callStatic` method provided by the **ethers.js** `Contract` instances. This is a useful method that submits a state-changing transaction to an Ethereum node, but asks the node to simulate the state change, rather than to execute it. Our script can then return the result of the simulated state change: -```typescript reference title="Getting Quotes from the Quoter contract" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/2e8fb5ef56e502d4eb0261e4abff262c33a30760/v3-sdk/quoting/src/libs/quote.ts#L21-L30 +```typescript +const quotedAmountOut = await quoterContract.callStatic.quoteExactInputSingle( + token0, + token1, + fee, + fromReadableAmount( + CurrentConfig.tokens.amountIn, + CurrentConfig.tokens.in.decimals + ).toString(), + 0 +) ``` +The `fromReadableAmount()` function creates the amount of the smallest unit of a token from the full unit amount and the decimals. + The result of the call is the number of output tokens you'd receive for the quoted swap. It should be noted that `quoteExactInputSingle` is only 1 of 4 different methods that the quoter offers: @@ -112,4 +194,4 @@ It should be noted that `quoteExactInputSingle` is only 1 of 4 different methods ## Next Steps -Now that you're able to make a quote, check out our next guide on [trading](./03-trading.md) using this quote! +Now that you're able to make a quote, check out our next guide on [trading](./02-trading.md) using this quote! diff --git a/docs/sdk/v3/guides/trading/03-trading.md b/docs/sdk/v3/guides/swaps/02-trading.md similarity index 50% rename from docs/sdk/v3/guides/trading/03-trading.md rename to docs/sdk/v3/guides/swaps/02-trading.md index 296fcc3b06..1bf835aa5c 100644 --- a/docs/sdk/v3/guides/trading/03-trading.md +++ b/docs/sdk/v3/guides/swaps/02-trading.md @@ -5,10 +5,10 @@ title: Executing a Trade ## Introduction -This guide will build off our [quoting guide](./02-quoting.md) and show how to use a quote to construct and execute a trade on the Uniswap V3 protocol. It is based on the [Trading code example](https://github.com/Uniswap/examples/tree/main/v3-sdk/trading), found in the Uniswap code examples [repository](https://github.com/Uniswap/examples). To run this example, check out the guide's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/README.md) and follow the setup instructions. +This guide will build off our [quoting guide](./01-quoting.md) and show how to use a quote to construct and execute a trade on the Uniswap V3 protocol. It is based on the [Trading code example](https://github.com/Uniswap/examples/tree/main/v3-sdk/trading), found in the Uniswap code examples [repository](https://github.com/Uniswap/examples). To run this example, check out the guide's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/README.md) and follow the setup instructions. :::info -If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](./01-background.md) page! +If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](../01-background.md) page! ::: In this example we will trade between two ERC20 tokens: **WETH and USDC**. The tokens, amount of input token, and the fee level can be configured as inputs. @@ -34,49 +34,123 @@ The core code of this guide can be found in [`trading.ts`](https://github.com/Un ## Constructing a route from pool information -To construct our trade, we will first create an model instance of a `Pool`. We will first extract the needed metadata from the relevant pool contract. Metadata includes both constant information about the pool as well as information about its current state stored in its first slot: +To construct our trade, we will first create an model instance of a `Pool`. We create an **ethers** contract like in the [previous guide](./01-quoting.md#referencing-the-pool-contract-and-fetching-metadata). + We will first extract the needed metadata from the relevant pool contract. Metadata includes both constant information about the pool as well as information about its current state stored in its first slot: + +```typescript +async function getPoolInfo() { + const [token0, token1, fee, tickSpacing, liquidity, slot0] = + await Promise.all([ + poolContract.token0(), + poolContract.token1(), + poolContract.fee(), + poolContract.tickSpacing(), + poolContract.liquidity(), + poolContract.slot0(), + ]) + + return { + token0, + token1, + fee, + tickSpacing, + liquidity, + sqrtPriceX96: slot0[0], + tick: slot0[1], + } +} -```typescript reference title="Fetching pool metadata" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/bbee4b974768ff1668ac56e27d1fe840060bb61b/v3-sdk/trading/src/libs/pool.ts#L38-L56 ``` Using this metadata along with our inputs, we will then construct a `Pool`: -```typescript reference title="Constructing a Pool" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/bbee4b974768ff1668ac56e27d1fe840060bb61b/v3-sdk/trading/src/libs/trading.ts#L41-L50 +```typescript +const poolInfo = await getPoolInfo() + +const pool = new Pool( + CurrentConfig.tokens.in, + CurrentConfig.tokens.out, + CurrentConfig.tokens.poolFee, + poolInfo.sqrtPriceX96.toString(), + poolInfo.liquidity.toString(), + poolInfo.tick +) ``` With this `Pool`, we can now construct a route to use in our trade. We will reuse our previous quoting code to calculate the output amount we expect from our trade: -```typescript reference title="Constructing a Route" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/bbee4b974768ff1668ac56e27d1fe840060bb61b/v3-sdk/trading/src/libs/trading.ts#L52-L56 +```typescript +import { Route } from '@uniswap/v3-sdk' + +const swapRoute = new Route( + [pool], + CurrentConfig.tokens.in, + CurrentConfig.tokens.out +) ``` ## Constructing an unchecked trade Once we have constructed the route object, we now need to obtain a quote for the given `inputAmount` of the example: -```typescript reference title="Getting a quote" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/bbee4b974768ff1668ac56e27d1fe840060bb61b/v3-sdk/trading/src/libs/trading.ts#L58 +```typescript +const amountOut = await getOutputQuote(swapRoute) ``` -As shown below, the quote is obtained using the `v3-sdk`'s `SwapQuoter`, in contrast to the [previous quoting guide](./02-quoting.md), where we directly accessed the smart contact: - -```typescript reference title="Fetching a quote using the v3-sdk" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/bbee4b974768ff1668ac56e27d1fe840060bb61b/v3-sdk/trading/src/libs/trading.ts#L128-L141 +As shown below, the quote is obtained using the `v3-sdk`'s `SwapQuoter`, in contrast to the [previous quoting guide](./01-quoting.md), where we directly accessed the smart contact: + +```typescript +import { SwapQuoter } from '@uniswap/v3-sdk' +import { CurrencyAmount, TradeType } from '@uniswap/sdk-core' + +const { calldata } = await SwapQuoter.quoteCallParameters( + swapRoute, + CurrencyAmount.fromRawAmount( + CurrentConfig.tokens.in, + fromReadableAmount( + CurrentConfig.tokens.amountIn, + CurrentConfig.tokens.in.decimals + ) + ), + TradeType.EXACT_INPUT, + { + useQuoterV2: true, + } +) ``` The `SwapQuoter`'s `quoteCallParameters` function, gives us the calldata needed to make the call to the `Quoter`, and we then decode the returned quote: -```typescript reference title="Getting a quote using the v3-sdk" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/bbee4b974768ff1668ac56e27d1fe840060bb61b/v3-sdk/trading/src/libs/trading.ts#L143-L148 -``` +```typescript +const quoteCallReturnData = await provider.call({ + to: QUOTER_CONTRACT_ADDRESS, + data: calldata, +}) +return ethers.utils.defaultAbiCoder.decode(['uint256'], quoteCallReturnData) +``` With the quote and the route, we can now construct an unchecked trade using the route in addition to the output amount from a quote based on our input: -```typescript reference title="Creating a Trade" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/bbee4b974768ff1668ac56e27d1fe840060bb61b/v3-sdk/trading/src/libs/trading.ts#L60-L74 +```typescript +import { Trade } from 'uniswap/v3-sdk' +import JSBI from 'jsbi' + +const uncheckedTrade = Trade.createUncheckedTrade({ + route: swapRoute, + inputAmount: CurrencyAmount.fromRawAmount( + CurrentConfig.tokens.in, + fromReadableAmount( + CurrentConfig.tokens.amountIn, + CurrentConfig.tokens.in.decimals + ) + ), + outputAmount: CurrencyAmount.fromRawAmount( + CurrentConfig.tokens.out, + JSBI.BigInt(amountOut) + ), + tradeType: TradeType.EXACT_INPUT, +}) ``` This example uses an exact input trade, but we can also construct a trade using exact output assuming we adapt our quoting code accordingly. @@ -85,28 +159,46 @@ This example uses an exact input trade, but we can also construct a trade using Once we have created a trade, we can now execute this trade with our provider. First, we must give the `SwapRouter` approval to spend our tokens for us: -```typescript reference title="Approve SwapRouter to spend our tokens" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/bbee4b974768ff1668ac56e27d1fe840060bb61b/v3-sdk/trading/src/libs/trading.ts#L90 +```typescript +const tokenApproval = await getTokenTransferApproval(CurrentConfig.tokens.in) ``` Then, we set our options that define how much time and slippage can occur in our execution as well as the address to use for our wallet: -```typescript reference title="Constructing SwapOptions" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/c4667fadb13584268bbee2e0e0f556558a474751/v3-sdk/trading/src/libs/trading.ts#L97-L101 +```typescript +import { SwapOptions } from '@uniswap/v3-sdk' +import { Percent } from '@uniswap/sdk-core' + +const options: SwapOptions = { + slippageTolerance: new Percent(50, 10_000), // 50 bips, or 0.50% + deadline: Math.floor(Date.now() / 1000) + 60 * 20, // 20 minutes from the current Unix time + recipient: walletAddress, +} ``` Next, we use the Uniswap `SwapRouter` to get the associated call parameters for our trade and options: -```typescript reference title="Getting call parameters" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/bbee4b974768ff1668ac56e27d1fe840060bb61b/v3-sdk/trading/src/libs/trading.ts#L103 +```typescript +import { SwapRouter } from '@uniswap/v3-sdk' + +const methodParameters = SwapRouter.swapCallParameters([uncheckedTrade], options) ``` Finally, we can construct a transaction from the method parameters and send the transaction: -```typescript reference title="Sending a transaction" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/bbee4b974768ff1668ac56e27d1fe840060bb61b/v3-sdk/trading/src/libs/trading.ts#L105-L114 +```typescript +const tx = { + data: methodParameters.calldata, + to: SWAP_ROUTER_ADDRESS, + value: methodParameters.value, + from: walletAddress, + maxFeePerGas: MAX_FEE_PER_GAS, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS, +} + +const res = await wallet.sendTransaction(tx) ``` ## Next Steps -The resulting example allows for trading between any two ERC20 tokens, but this can be suboptimal for the best pricing and fees. To achieve the best possible price, we use the Uniswap auto router to route through pools to get an optimal cost. Our [routing](./04-routing.md) guide will show you how to use this router and execute optimal swaps. +The resulting example allows for trading between any two ERC20 tokens, but this can be suboptimal for the best pricing and fees. To achieve the best possible price, we use the Uniswap auto router to route through pools to get an optimal cost. Our [routing](./03-routing.md) guide will show you how to use this router and execute optimal swaps. diff --git a/docs/sdk/v3/guides/swaps/03-routing.md b/docs/sdk/v3/guides/swaps/03-routing.md new file mode 100644 index 0000000000..e1d7dd9fa1 --- /dev/null +++ b/docs/sdk/v3/guides/swaps/03-routing.md @@ -0,0 +1,162 @@ +--- +id: routing +title: Routing a Swap +--- + +## Introduction + +This guide will cover how to use Uniswap's smart order router to compute optimal routes and execute swaps. Rather than trading between a single pool, smart routing may use multiple hops (as many as needed) to ensure that the end result of the swap is the optimal price. It is based on the [routing code example](https://github.com/Uniswap/examples/tree/main/v3-sdk/routing), found in the Uniswap code examples [repository](https://github.com/Uniswap/examples). To run this example, check out the guide's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/routing/README.md) and follow the setup instructions. + +:::info +If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](./01-background.md) page! +::: + +In this example we will trade between **WETH and USDC**, but you can configure your example to us any two currencies and amount of input currency. + +The guide will **cover**: + +1. Creating a router instance +2. Creating a route +3. Swapping using a route + +At the end of the guide, we should be able to create a route and and execute a swap between any two currencies tokens using the example's included UI. + +For this guide, the following Uniswap packages are used: + +- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) +- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) +- [`@uniswap/smart-order-router`](https://www.npmjs.com/package/@uniswap/smart-order-router) + +The core code of this guide can be found in [`routing.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/routing/src/libs/routing.ts) + +The config, which we will use in some code snippets in this guides has this structure: + +```typescript +import { Token } from '@uniswap/sdk-core' + +interface ExampleConfig { + env: Environment + rpc: { + local: string + mainnet: string + } + wallet: { + address: string + privateKey: string + } + tokens: { + in: Token + amountIn: number + out: Token + } +} + +export const CurrentConfig: ExampleConfig = {...} +``` + +## Creating a router instance + +To compute our route, we will use the `@uniswap/smart-order-router` package, specifically the `AlphaRouter` class which requires a `chainId` and a `provider`. Note that routing is not supported for local forks, so we will use a mainnet provider even when swapping on a local fork: + +```typescript +import { AlphaRouter, ChainId } from '@uniswap/smart-order-router' + +const provider = new ethers.providers.JsonRpcProvider(rpcUrl) + +const router = new AlphaRouter({ + chainId: ChainId.MAINNET, + provider, +}) +``` + +## Creating a route + +Next, we will create our options conforming to the `SwapOptionsSwapRouter02` interface, defining the wallet to use, slippage tolerance, and deadline for the transaction: + +```typescript +import { SwapOptionsSwapRouter02, SwapType } from '@uniswap/smart-order-router' +import { Percent } from '@uniswap/sdk-core' + +const options: SwapOptionsSwapRouter02 = { + recipient: CurrentConfig.wallet.address, + slippageTolerance: new Percent(50, 10_000), + deadline: Math.floor(Date.now() / 1000 + 1800), + type: SwapType.SWAP_ROUTER_02, +} +``` + +Using these options, we can now create a trade (`TradeType.EXACT_INPUT` or `TradeType.EXACT_OUTPUT`) with the currency and the input amount to use to get a quote. For this example, we'll use an `EXACT_INPUT` trade to get a quote outputted in the quote currency. + +```typescript +import { CurrencyAmount } from '@uniswap/sdk-core' + +const rawTokenAmountIn: JSBI = fromReadableAmount( + CurrentConfig.currencies.amountIn, + CurrentConfig.currencies.in.decimals + ) + +const route = await router.route( + CurrencyAmount.fromRawAmount( + CurrentConfig.currencies.in, + rawTokenAmountIn + ), + CurrentConfig.currencies.out, + TradeType.EXACT_INPUT, + options +) +``` + +The `fromReadableAmount` function calculates the amount of tokens in the Token's smallest unit from the full unit and the Token's decimals. + +`route` and `route.methodParameters` are *optional* as the request can fail, for example if no route exists between the two Tokens or because of networking issues. +We check if the call was succesful: + +```typescript +if (!route || !route.methodParameters) { + // Handle failed request +} +``` + +Depending on our preferences and reason for the issue we could retry the request or throw an Error. + +## Swapping using a route + +First, we need to give approval to the `SwapRouter` smart contract to spend our tokens for us: + +```typescript +import { ethers } from 'ethers' +... + +const wallet = new ethers.Wallet(privateKey, provider) +const tokenContract = new ethers.Contract( + CurrentConfig.tokens.in.address, + ERC20ABI, + wallet +) +const tokenApproval = await tokenContract.approve( + V3_SWAP_ROUTER_ADDRESS, + ethers.BigNumber.from(rawTokenAmountIn.toString()) +) +``` + +We can get the **V3_SWAP_ROUTER_ADDRESS** for our chain from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). We need to wait one block for the approval transaction to be included by the blockchain. + +Once the approval has been granted, we can now execute the trade using the route's computed calldata, values, and gas values: + + +```typescript +const txRes = await wallet.sendTransaction({ + data: route.methodParameters.calldata, + to: V3_SWAP_ROUTER_ADDRESS, + value: route.methodParameters.value, + from: wallet.address, + maxFeePerGas: MAX_FEE_PER_GAS, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS, +}) +``` + +After swapping, you should see the currency balances update in the UI shortly after the block is confirmed. + +## Next Steps + +Now that you're familiar with trading, consider checking out our next guides on [pooling liquidity](../liquidity/01-position-data.md) to Uniswap! diff --git a/docs/sdk/v3/guides/swaps/_category_.json b/docs/sdk/v3/guides/swaps/_category_.json new file mode 100644 index 0000000000..16e66adb14 --- /dev/null +++ b/docs/sdk/v3/guides/swaps/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Swaps", + "position": 2, + "collapsed": true +} diff --git a/docs/sdk/v3/guides/trading/04-routing.md b/docs/sdk/v3/guides/trading/04-routing.md deleted file mode 100644 index 41d4bb24dd..0000000000 --- a/docs/sdk/v3/guides/trading/04-routing.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -id: routing -title: Routing a Swap ---- - -## Introduction - -This guide will cover how to use Uniswap's smart order router to compute optimal routes and execute swaps. Rather than trading between a single pool, smart routing may use multiple hops (as many as needed) to ensure that the end result of the swap is the optimal price. It is based on the [routing code example](https://github.com/Uniswap/examples/tree/main/v3-sdk/routing), found in the Uniswap code examples [repository](https://github.com/Uniswap/examples). To run this example, check out the guide's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/routing/README.md) and follow the setup instructions. - -:::info -If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](./01-background.md) page! -::: - -In this example we will trade between **WETH and USDC**, but you can configure your example to us any two currencies and amount of input currency. - -The guide will **cover**: - -1. Creating a router instance -2. Creating a route -3. Swapping using a route - -At the end of the guide, we should be able to create a route and and execute a swap between any two currencies tokens using the example's included UI. - -For this guide, the following Uniswap packages are used: - -- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) -- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) -- [`@uniswap/smart-order-router`](https://www.npmjs.com/package/@uniswap/smart-order-router) - -The core code of this guide can be found in [`routing.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/routing/src/libs/routing.ts) - -## Creating a router instance - -To compute our route, we will use the `@uniswap/smart-order-router` package, specifically the `AlphaRouter` class which requires a `chainId` and a `provider`. Note that routing is not supported for local forks, so we will use a mainnet provider even when swapping on a local fork: - -```typescript reference title="Instantiating an AlphaRouter" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/38ff60aeb3ad8ff839db9e7952a726ca7d6b68fd/v3-sdk/routing/src/libs/routing.ts#L24-L27 -``` - -## Creating a route - -Next, we will create our options conforming to the `SwapOptionsSwapRouter02` interface, defining the wallet to use, slippage tolerance, and deadline for the transaction: - -```typescript reference title="Routing Options" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/c4667fadb13584268bbee2e0e0f556558a474751/v3-sdk/routing/src/libs/routing.ts#L33-L38 -``` - -Using these options, we can now create a trade (`TradeType.EXACT_INPUT` or `TradeType.EXACT_OUTPUT`) with the currency and the input amount to use to get a quote. For this example, we'll use an `EXACT_INPUT` trade to get a quote outputted in the quote currency. - -```typescript reference title="Creating a route" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/38ff60aeb3ad8ff839db9e7952a726ca7d6b68fd/v3-sdk/routing/src/libs/routing.ts#L36-L47 -``` - -## Swapping using a route - - -First, we need to give approval to the `SwapRouter` smart contract to spend our tokens for us: - -```typescript reference title="Approving SwapRouter to spend our tokens" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/0071bb5883fba6f4cc39a5f1644ac941e4f24822/v3-sdk/routing/src/libs/routing.ts#L66 -``` - -Once the approval has been granted and using the route, we can now execute the trade using the route's computed calldata, values, and gas values: - - -```typescript reference title="Using a route" referenceLinkText="View on Github" customStyling -https://github.com/Uniswap/examples/blob/38ff60aeb3ad8ff839db9e7952a726ca7d6b68fd/v3-sdk/routing/src/libs/routing.ts#L61-L68 -``` - -After swapping, you should see the currency balances update in the UI shortly after the block is confirmed. - -## Next Steps - -Now that you're familiar with trading, consider checking out our next guides on [pooling liquidity](./liquidity/01-minting-position.md) to Uniswap! diff --git a/docs/sdk/v3/guides/trading/_category_.json b/docs/sdk/v3/guides/trading/_category_.json deleted file mode 100644 index c60d7589d6..0000000000 --- a/docs/sdk/v3/guides/trading/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "Trading", - "position": 2, - "collapsed": false -} From 7b199e96df7e239b8bf25bf44bb2a3d1ca37bad4 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Fri, 30 Jun 2023 19:12:07 +0100 Subject: [PATCH 20/52] Fix some typos and improve formatting --- docs/sdk/v3/guides/01-background.md | 4 ++-- .../v3/guides/liquidity/01-position-data.md | 20 +++++++++++++++---- docs/sdk/v3/guides/swaps/01-quoting.md | 12 ++++++----- docs/sdk/v3/guides/swaps/02-trading.md | 4 ++++ docs/sdk/v3/guides/swaps/03-routing.md | 5 ++--- 5 files changed, 31 insertions(+), 14 deletions(-) diff --git a/docs/sdk/v3/guides/01-background.md b/docs/sdk/v3/guides/01-background.md index f2108d3caa..992cc659b9 100644 --- a/docs/sdk/v3/guides/01-background.md +++ b/docs/sdk/v3/guides/01-background.md @@ -7,7 +7,7 @@ position: 1 Before integrating with Uniswap, it may be helpful for newcomers to review the following background information on some important developer web3 concepts, the structure of our examples, and SDK concepts. :::info -Already familiar with web3 development and/or the basics of our SDK and want to get right to the code? Start with our first guide, [Getting a Quote](./trading/01-quoting.md)! +Already familiar with web3 development and/or the basics of our SDK and want to get right to the code? Start with our first guide, [Getting a Quote](./swaps/01-quoting.md)! ::: ## Providers @@ -16,7 +16,7 @@ Communication with the blockchain is typically done through a provider and local To achieve this, our examples use the [ethers.js](https://docs.ethers.io/v5/) library. To instantiate a provider you will need a data source. Our examples offer two options: -- **JSON RPC URL**: If you are working directly with the Ethereum mainnet or a local fork, products such as [infura](https://infura.io/) offer JSON RPC URLs for a wide variety of chains and testnets. For our examples, we'll only be using the Ethereum mainnet. +- **JSON RPC URL**: If you are working directly with the Ethereum mainnet or a local fork, products such as [Infura](https://infura.io/) offer JSON RPC URLs for a wide variety of chains and testnets. For our examples, we'll only be using the Ethereum mainnet. - **Wallet Extension**: If you are connecting to a wallet browser extension, these wallets embed a source directly into the Javascript window object as `window.ethereum`. This object surfaces information about the user's wallets and provides the ability to communicate with the connected chain. Importantly for our examples, it can be used with `ethers.js` to construct a provider. diff --git a/docs/sdk/v3/guides/liquidity/01-position-data.md b/docs/sdk/v3/guides/liquidity/01-position-data.md index ebf5c8d59b..55477da566 100644 --- a/docs/sdk/v3/guides/liquidity/01-position-data.md +++ b/docs/sdk/v3/guides/liquidity/01-position-data.md @@ -5,13 +5,13 @@ title: Liquidity Positions ## Introduction -This guide will introduce us to liquidity positions in Uniswap V3 and present the `v3-sdk` classes and Contracts used to interact with the protocol. +This guide will introduce us to **liquidity positions** in Uniswap V3 and present the `v3-sdk` classes and Contracts used to interact with the protocol. The concepts and code snippets showcased here can be found across the **Pooling Liquidity** examples in the Uniswap code examples [repository](https://github.com/Uniswap/examples). In this guide, we will take a look at the [Position](../../reference/classes/Position.md) and [NonfungiblePositionManager](../../reference/classes/NonfungiblePositionManager.md) classes, as well as the [NonfungiblePositionManager Contract](../../../../contracts/v3/reference/periphery/NonfungiblePositionManager.md). At the end of the guide, we should be familiar with the most important classes used to interact with liquidity positions. -We should also understand how to fetch and create positions on the Contract side. +We should also understand how to fetch positions from the **NonfungiblePositionManager Contract**. For this guide, the following Uniswap packages are used: @@ -91,7 +91,7 @@ const singleSidePositionToken0 = Position.fromAmount0({ const amount1: BigIntish = 100000000 -const singleSidePositionToken1 = Position.fromAmount0({ +const singleSidePositionToken1 = Position.fromAmount1({ pool, tickLower, tickUpper, @@ -106,6 +106,8 @@ A create transaction would then fail if the wallet doesn't hold enough `token1` All of these functions take an Object with **named values** as a call parameter. The amount and liquidity values are of type `BigIntish` which accepts `number`, `string` and `JSBI`. +The values of `tickLower` and `tickUpper` must match **initializable ticks** of the Pool. + ## NonfungiblePositionManager The `NonfungiblePositionManager` class is mainly used to create calldata for functions on the **NonfungiblePositionManager Contract**. @@ -150,11 +152,21 @@ const { calldata, value } = NonfungiblePositionManager.removeCallParameters( Take a look at the [Modify Positions guide](04-modifying-position.md) to learn how to create the `currentPosition` and `removeLiquidityOptions` parameters. +### Collecting Fees + +To collect fees accrued, the `collect` function is called on the Contract. +The **sdk class** provides the `collectCallParameters` function to create the calldata for that: + +```typescript +const { calldata, value } = + NonfungiblePositionManager.collectCallParameters(collectOptions) +``` + ## Fetching Positions The [NonfungiblePositionManager Contract](../../../../contracts/v3/reference/periphery/NonfungiblePositionManager.md) can be used to create Positions, as well as get information on existing Positions. -In this section we will fetch all positions for an address. +In this section we will **fetch all Positions** for an address. ### Creating an ethers Contract diff --git a/docs/sdk/v3/guides/swaps/01-quoting.md b/docs/sdk/v3/guides/swaps/01-quoting.md index d5494efcab..94b338539e 100644 --- a/docs/sdk/v3/guides/swaps/01-quoting.md +++ b/docs/sdk/v3/guides/swaps/01-quoting.md @@ -8,7 +8,7 @@ title: Getting a Quote This guide will cover how to get the current quotes for any token pair on the Uniswap protocol. It is based on the [Quoting code example](https://github.com/Uniswap/examples/tree/main/v3-sdk/quoting), found in the Uniswap code examples [repository](https://github.com/Uniswap/examples). To run this example, check out the examples's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/minting-position/README.md) and follow the setup instructions. :::info -If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](./01-background.md) page! +If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](../01-background.md) page! ::: In this example we will use `quoteExactInputSingle` to get a quote for the pair **USDC - WETH**. @@ -51,9 +51,7 @@ interface ExampleConfig { } } -export CurrentConfig: ExampleConfig = { - ... -} +export const CurrentConfig: ExampleConfig = {...} ``` ## Computing the Pool's deployment address @@ -62,6 +60,8 @@ To interact with the **USDC - WETH** Pool contract, we first need to compute its The SDK provides a utility method for that: ```typescript +import { computePoolAddress } from '@uniswap/v3-sdk' + const currentPoolAddress = computePoolAddress({ factoryAddress: POOL_FACTORY_CONTRACT_ADDRESS, tokenA: CurrentConfig.tokens.in, @@ -127,10 +127,12 @@ Having constructed our reference to the contract, we can now access its methods We use a batch `Promise` call. This approach queries state data concurrently, rather than sequentially, to minimize the chance of fetching out of sync data that may be returned if sequential queries are executed over the span of two blocks: ```typescript -const [token0, token1, fee] = await Promise.all([ +const [token0, token1, fee, liquidity, slot0] = await Promise.all([ poolContract.token0(), poolContract.token1(), poolContract.fee(), + poolContract.liquidity(), + poolContract.slot0(), ]) ``` diff --git a/docs/sdk/v3/guides/swaps/02-trading.md b/docs/sdk/v3/guides/swaps/02-trading.md index 1bf835aa5c..083830b2b8 100644 --- a/docs/sdk/v3/guides/swaps/02-trading.md +++ b/docs/sdk/v3/guides/swaps/02-trading.md @@ -134,6 +134,7 @@ With the quote and the route, we can now construct an unchecked trade using the ```typescript import { Trade } from 'uniswap/v3-sdk' +import { CurrencyAmount, TradeType } from '@uniswap/sdk-core' import JSBI from 'jsbi' const uncheckedTrade = Trade.createUncheckedTrade({ @@ -163,6 +164,9 @@ Once we have created a trade, we can now execute this trade with our provider. F const tokenApproval = await getTokenTransferApproval(CurrentConfig.tokens.in) ``` +You can find the approval function [here](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/src/libs/trading.ts#L151). +We will use this function or similar implementations in most guides. + Then, we set our options that define how much time and slippage can occur in our execution as well as the address to use for our wallet: ```typescript diff --git a/docs/sdk/v3/guides/swaps/03-routing.md b/docs/sdk/v3/guides/swaps/03-routing.md index e1d7dd9fa1..1b823d1eb6 100644 --- a/docs/sdk/v3/guides/swaps/03-routing.md +++ b/docs/sdk/v3/guides/swaps/03-routing.md @@ -88,7 +88,7 @@ const options: SwapOptionsSwapRouter02 = { Using these options, we can now create a trade (`TradeType.EXACT_INPUT` or `TradeType.EXACT_OUTPUT`) with the currency and the input amount to use to get a quote. For this example, we'll use an `EXACT_INPUT` trade to get a quote outputted in the quote currency. ```typescript -import { CurrencyAmount } from '@uniswap/sdk-core' +import { CurrencyAmount, TradeType } from '@uniswap/sdk-core' const rawTokenAmountIn: JSBI = fromReadableAmount( CurrentConfig.currencies.amountIn, @@ -108,7 +108,7 @@ const route = await router.route( The `fromReadableAmount` function calculates the amount of tokens in the Token's smallest unit from the full unit and the Token's decimals. -`route` and `route.methodParameters` are *optional* as the request can fail, for example if no route exists between the two Tokens or because of networking issues. +`route` and `route.methodParameters` are *optional* as the request can fail, for example if **no route exists** between the two Tokens or because of networking issues. We check if the call was succesful: ```typescript @@ -143,7 +143,6 @@ We can get the **V3_SWAP_ROUTER_ADDRESS** for our chain from [Github](https://gi Once the approval has been granted, we can now execute the trade using the route's computed calldata, values, and gas values: - ```typescript const txRes = await wallet.sendTransaction({ data: route.methodParameters.calldata, From 2192856a3a8a362a78e19f85b481f734fe53e917 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Sat, 1 Jul 2023 13:53:30 +0100 Subject: [PATCH 21/52] Some typo, oversights and formatting fixes in advanced v3-sdk guides --- docs/sdk/v3/guides/advanced/01-overview.md | 5 ++- docs/sdk/v3/guides/advanced/02-pool-data.md | 33 +++++++++------ .../v3/guides/advanced/03-active-liquidity.md | 41 ++++++++++++------- .../sdk/v3/guides/advanced/04-price-oracle.md | 17 ++++---- .../sdk/v3/guides/advanced/05-range-orders.md | 15 ++++--- 5 files changed, 69 insertions(+), 42 deletions(-) diff --git a/docs/sdk/v3/guides/advanced/01-overview.md b/docs/sdk/v3/guides/advanced/01-overview.md index f02ec5c5d1..477304f783 100644 --- a/docs/sdk/v3/guides/advanced/01-overview.md +++ b/docs/sdk/v3/guides/advanced/01-overview.md @@ -11,6 +11,7 @@ If you need a briefer on the SDK and to learn more about how these guides connec The following examples use **ethersJS** and the **Uniswap V3 subgraph** hosted on The Graph's hosted service. To learn more about Uniswap's subgraphs, visit the [API](../../../../api/subgraph/overview.md) section. -We will take a deep dive into the Uniswap V3 protocol and use practical examples to understand the data stored by the Uniswap smart contracts. We will explore how we can compute the available liquidity in a specific price range, visualize **liquidity density** in pools and how to use Uniswap as a **price oracle**. +We will take a deep dive into the Uniswap V3 protocol and use practical examples to understand the data stored by the Uniswap smart contracts. +We will explore how we can compute the available liquidity in a specific price range, visualize **liquidity density** in pools, use Uniswap as a **price oracle** and swap by creating **Range Orders**. -These guides are a bit longer than the previous ones and provide more theoratical background and complete code examples. +These guides are a bit longer than the previous ones and provide more theoretical background. diff --git a/docs/sdk/v3/guides/advanced/02-pool-data.md b/docs/sdk/v3/guides/advanced/02-pool-data.md index 1b454a9991..1680550b7c 100644 --- a/docs/sdk/v3/guides/advanced/02-pool-data.md +++ b/docs/sdk/v3/guides/advanced/02-pool-data.md @@ -27,7 +27,7 @@ For this guide, the following Uniswap packages are used: - [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) - [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) -The core code of this guide can be found in [`pool.ts`](https://github.com/Uniswap/v3-sdk/blob/main/src/entities/pool.ts) +The core code of this guide can be found in [`pool.ts`](https://github.com/Uniswap/examples/tree/main/v3-sdk/pool-data/src/lib/pool.ts) ## Computing the Pool's deployment address @@ -40,10 +40,10 @@ import { Token, WETH9 } from '@uniswap/sdk-core' const USDC = new Token(1, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 6) const WETH = WETH9[USDC.chainId] const poolAddress = Pool.getAddress( - tokenA, - tokenB, + USDC, + WETH, FeeAmount.LOW - ) + ) ``` Uniswap V3 allows different Fee tiers when deploying a pool, so multiple pools can exist for each pair of tokens. @@ -80,19 +80,28 @@ The [slot0 function](../../../../contracts/v3/reference/core/interfaces/pool/IUn ```solidity function slot0( - ) external view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked) + ) external view returns ( + uint160 sqrtPriceX96, + int24 tick, + uint16 observationIndex, + uint16 observationCardinality, + uint16 observationCardinalityNext, + uint8 feeProtocol, + bool unlocked + ) ``` For our use case, we only need the `sqrtPriceX96` and the currently active `tick`. - ## Fetching all Ticks V3 pools use ticks to [concentrate liquidity](../../../concepts/protocol/concentrated-liquidity.md) in price ranges and allow for better pricing of trades. -Even though most Pools only have a couple of initialized ticks, it is possible that a pools liquidity is spread among thousands of ticks. +Even though most Pools only have a couple of **initialized ticks**, it is possible that a pools liquidity is defined by thousands of **initialized ticks**. In that case, it can be very expensive or slow to get all of them with RPC calls. -To fetch all ticks of the **USDC - WETH Pool**, we will use the [Uniswap V3 graph](../../../api/subgraph/overview.md). To construct a `Tick` for the SDK, we need the **tickIdx**, the **liquidityGross** and the **liquidityNet**. +To fetch all ticks of the **USDC - WETH Pool**, we will use the [Uniswap V3 graph](../../../../api/subgraph/overview.md). +To construct a `Tick` for the SDK, we need the **tickIdx**, the **liquidityGross** and the **liquidityNet**. + We define our GraphQL query and [send a POST request](https://axios-http.com/docs/post_example) to the V3 subgraph API endpoint: ```typescript @@ -119,7 +128,7 @@ axios.post( ) ``` -We only fetch the ticks that have liquidity, and we convert the poolAddress to lower case for the subgraph to work with. To make sure the Ticks are ordered correctly, we also define the order direction in the query. +We only fetch the ticks that **have liquidity**, and we convert the poolAddress to **lower case** for the subgraph to work with. To make sure the Ticks are ordered correctly, we also define the **order direction** in the query. To create our Pool, we need to map the raw data we received from the subgraph to `Tick` objects that the SDK can work with: @@ -141,8 +150,8 @@ We have everything to construct our `Pool` now: ```typescript const usdcWethPool = new Pool( - tokenA, - tokenB, + USDC, + WETH, feeAmount, slot0.sqrtPriceX96, liquidity, @@ -155,4 +164,4 @@ With this fully initialized Pool, we can calculate swaps on it offchain, without ## Next Steps -Now that you are familiar with using TheGraph, continue your journey with the next example on visualizing the Liquidity density of a pool. +Now that you are familiar with using TheGraph, continue your journey with the [next example](./03-active-liquidity.md) on visualizing the Liquidity density of a pool. diff --git a/docs/sdk/v3/guides/advanced/03-active-liquidity.md b/docs/sdk/v3/guides/advanced/03-active-liquidity.md index c5f8d573d2..06821c3b7e 100644 --- a/docs/sdk/v3/guides/advanced/03-active-liquidity.md +++ b/docs/sdk/v3/guides/advanced/03-active-liquidity.md @@ -36,13 +36,16 @@ To visualize the distribution of active liquidity in our Pool, we want to draw o When providing liquidity for a pool, the LP decides the **price range** in which the liquidity should be provided, and the amount of liquidity to be provided. The pool understands the position as **liquidity between the lower and upper Tick**. The Tick Index in this context is a representation of the price between the Pool's assets. + Looking at this [visualization](https://www.desmos.com/calculator/oduetjzfp4) of multiple positions in a V3 Pool, we can see that the liquidity available for a swap does not change inside a position, but when crossing into the next position. This is what the **Initialized Ticks** of a Pool represent - they are a representation of the start or end of one or more positions. LiquidityNet1 -When entering or leaving a position, its liquidity is added or correspondingly removed from the active liquidity available for a Swap. The initialized Ticks store this change in available liquidity in the `liquidityNet` field. -The change is always stored in relation to the currently active Tick - the current price. When the price crosses an initialized Tick, it gets updated and liqudity that was previously added when crossing the Tick would now be removed and vice versa. +When entering or leaving a position, its liquidity is added or removed from the **active liquidity available** for a Swap. +The initialized Ticks store this change in available liquidity in the `liquidityNet` field. +The change is always stored in relation to the currently active Tick - the current price. +When the price crosses an initialized Tick, it gets updated and liqudity that was previously added when crossing the Tick would now be removed and vice versa. We already fetched the initialized Ticks of our Pool in the [previous guide](./02-pool-data.md). The format we got from the Graph is: @@ -56,12 +59,17 @@ interface GraphTick { ### Current Tick -The current Tick of the Pool represents the price after the last swap. Considering that the initialized Ticks only represent positions, we see that it is not necessarily one of the initialized Ticks but can be at any point in between them. -The active liqudity at the current Price is also stored in the smart contract - we already fetched it with the slot0 function. +The current Tick of the Pool represents the **current Price** after the last swap. +Considering that the initialized Ticks only represent positions, we see that it is not necessarily one of the initialized Ticks but can be at any point in between them. +The active liqudity at the current Price is also stored in the smart contract - we already fetched it with the `liquidity` function. ### Tickspacing -Only the Ticks with indices that are dividable by the tickspacing of a Pool are initializable. The Tickspacing of the Pool is dependent on the Fee Tier. Pools with lower fees are meant to be used for more stable Token Pairs and allow for more granularity in where LPs position their liquidity. We can get the `tickSpacing`from our pool: +Only the Ticks with indices that are dividable with 0 remainder by the tickspacing of a Pool are initializable. +The Tickspacing of the Pool is dependent on the Fee Tier. +Pools with lower fees are meant to be used for more stable Token Pairs and allow for more granularity in where LPs position their liquidity. + +We can get the `tickSpacing`from our pool: ```typescript const tickSpacing = pool.tickSpacing @@ -75,11 +83,12 @@ Instead, we will display a sensible number of Ticks around the current price. ## Calculating active liquidity -We know the spacing between Ticks and the Initialized Ticks where active liquidity changes. All we have to do is start calculating from the current Tick and iterate outwards. +We know the spacing between Ticks and the Initialized Ticks where active liquidity changes. +All we have to do is start calculating from the current Tick and iterate outwards. + To draw our chart we want a data structure that looks something like this: ```typescript - interface TickProcessed { tickIdx: number, liquidityActive: JSBI, @@ -118,8 +127,9 @@ const activeTickProcessed: TickProcessed = { } ``` -Here we also calculate the price of the tokens from the tickIdx, the `v3-sdk` exports a handy utility function for that. -We store it as a string as we won't make any further calculations in this example. +Here we also calculate the price of the tokens from the tickIdx, the `v3-sdk` exports a handy utility function for that, `tickToPrice`. +We store the Price as a string as we won't make any further calculations in this example. We will instead use it to display prices in the tooltip of our chart. +Notice how the `price0` is the Price of tokenA in terms of tokenB and the `price1` is the Price of tokenB in terms of tokenA **at the specified Tick**. If the **current Tick is initialized**, we also need to set the **liquidityNet** to correctly handle moving out of the position: @@ -162,7 +172,7 @@ for (let i = 0; i < 100; i++) { } ``` -We calculate one Tick at a time, and we need to make sure our Tick stays inside the possible price range. +We calculate one Tick at a time, and we need to make sure our Tick stays inside the possible price range by checking against `TickMath.MAX_TICK`. Again, we check if our current Tick is initialized and if so, recalculate the active liquidity: ```typescript @@ -187,6 +197,7 @@ for (let i = 0; i < 100; i++) { After we are done calculating the next 100 Ticks after the current Tick, we iterate in the opposite direction for the previous Ticks. Iterating downwards, we need to subtract the net liquidity where we added it when iterating upwards. You can find a full code example in the [Uniswap Example repository](https://github.com/Uniswap/examples/tree/main/v3-sdk/active-liquidity). + We are finally able to combine the previous, active and subsequent Ticks: ```typescript @@ -204,6 +215,8 @@ const chartTicks: TicksChart[] = allProcessedTicks.map((tickProcessed) => { ``` The loss of precision will not be visually noticeable in the chart and we are still able to display the exact number in a Tooltip if we wish to. +Liquidity is stored in a `uint128` format onchain, so the maximum loss of precision will be far smaller than the number of decimals of almost any ERC20 Token. + Finally, we draw the Chart: ```jsx @@ -232,13 +245,13 @@ You can also take a look at the [Uniswap Info](https://github.com/Uniswap/v3-inf ## Locked Liquidity -If you run the example, you will notice that the chart also displays a custom tooltip with additional information that we didn't touch on in this example. +If you run the example, you will notice that the chart also displays a custom tooltip with additional information that we didn't touch on in this example. The total locked liqudity in the tooltip represents the sum of positions in the currency locked at the selected Tick. -It is calculated as the maximum token output of a swap when crossing to the next Tick. +It is calculated as the maximum token output of a swap when crossing to the next Tick. The V3 pool here is initialized with only the liquidity of the current Tick. -Depending on your use case, it may make sense to display this value. You can find the full code in the [code example](https://github.com/Uniswap/examples/tree/main/v3-sdk/active-liquidity) +Depending on your use case, it may make sense to display this value. You can find the full code in the [code example](https://github.com/Uniswap/examples/tree/main/v3-sdk/active-liquidity). ## Next Steps -Now that you are familiar with liquidity data, consider checking out our next guide on using Uniswap as a Price Oracle. \ No newline at end of file +Now that you are familiar with liquidity data, consider checking out our [next guide](./04-price-oracle.md) on using Uniswap as a Price Oracle. diff --git a/docs/sdk/v3/guides/advanced/04-price-oracle.md b/docs/sdk/v3/guides/advanced/04-price-oracle.md index a3753ff177..3c73bc056d 100644 --- a/docs/sdk/v3/guides/advanced/04-price-oracle.md +++ b/docs/sdk/v3/guides/advanced/04-price-oracle.md @@ -13,8 +13,8 @@ To run this example, check out the guide's [README](https://github.com/Uniswap/e If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](./01-background.md) page! ::: -In this example we will use **ethers JS** to observe the development of a Pool's current tick over several blocks. -We will then calculate the time weighted average price - TWAP, and time weighted average liquidity - TWAL over the observed time interval. +In this example we will use **ethers JS** to observe the development of a Pool's current tick over several blocks. +We will then calculate the time weighted average price - **TWAP**, and time weighted average liquidity - **TWAL** over the observed time interval. This guide will **cover**: @@ -147,7 +147,7 @@ const secondsBetween = 108 const averageTick = diffTickCumulative / secondsBetween ``` -Now that we know the average active Tick over the last 10 blocks, we can calculate the price with the `tickToPrice` function, which returns a [`Price`](../../../core/reference/classes/Price.md) Object. Check out the [Pool data](./02-pool-data.md) guide to understand how to construct a Pool Object and access its properties. +Now that we know the average active Tick over the last 10 blocks, we can calculate the price with the `tickToPrice` function, which returns a [`Price`](../../../core/reference/classes/Price.md) Object. Check out the [Pool data](./02-pool-data.md) guide to understand how to construct a Pool Object and access its properties. We don't need the full Tick Data for this guide. ```typescript import { tickToPrice, Pool } from '@uniswap/v3-sdk' @@ -176,7 +176,7 @@ uint128 secondsPerLiquidityX128 = (uint160(delta) << 128) / liquidity uint160 secondsPerLiquidityCumulativeX128 = last.secondsPerLiquidityCumulativeX128 + secondsPerLiquidityX128 ``` -`last` is the most recent Observation in this illustrative code snippet. Consider taking a look at the [Oracle library](https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/Oracle.sol) to see the actual implementation. +`last` is the most recent Observation in this illustrative code snippet. Consider taking a look at the [Solidity Oracle library](https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/Oracle.sol) to see the actual implementation. Let's invert this calculation and find the average active liquidity over our observed time period. @@ -259,13 +259,14 @@ const observations = results.map((result) => { We now have an Array of observations in the same format that we are used to. :::note -Because Observations are stored in a **fixed size array**, they are **not sorted**. We need to sort the result by the timestamp. +Because Observations are stored in a **fixed size array** with always the oldest Observation overwritten if a new one is stored, they are **not sorted**. +We need to sort the result by the timestamp. ::: -The timestamps of the Observations we got are correspondent to blocks where Swaps happened on the Pool. -Because of this, we would need to calculate Observations for specific intervals manually from the surrounding Observations. +The timestamps of the Observations we got are correspondent to blocks where **Swaps or Position changes** happened on the Pool. +Because of this, we would need to calculate Observations for specific intervals manually from the **surrounding Observations**. -Overall, it is much harder to work with `observations` than with `observe`, and we need to consider multiple edge cases. +In conclusion, it is much harder to work with `observations` than with `observe`, and we need to consider multiple edge cases. For this reason, it is recommended to use the `observe` function. ## Next Steps diff --git a/docs/sdk/v3/guides/advanced/05-range-orders.md b/docs/sdk/v3/guides/advanced/05-range-orders.md index 6602fb776b..9cf35a7005 100644 --- a/docs/sdk/v3/guides/advanced/05-range-orders.md +++ b/docs/sdk/v3/guides/advanced/05-range-orders.md @@ -5,12 +5,12 @@ title: Range Orders ## Introduction -This guide will cover how single-side liquidity provisioning can be used to execute Limit Orders on Uniswap V3 Pools. +This guide will cover how single-side liquidity provisioning can be used to execute **Limit Orders** on Uniswap V3 Pools. An example to showcase this concept can be found in the [Range Order example](https://github.com/Uniswap/examples/tree/main/v3-sdk/range-order), in the Uniswap code examples [repository](https://github.com/Uniswap/example). To run this example, check out the guide's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/price-oracle/README.md) and follow the setup instructions. :::info -This guide builds on top of the [Pooling Liquidity guides](../liquidity/01-minting-position.md). +This guide builds on top of the [Pooling Liquidity guides](../liquidity/01-position-data.md). We recommend going through this section of the docs before imnplementing Range Orders. ::: @@ -56,7 +56,8 @@ function positions( uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint128 tokensOwed0, - uint128 tokensOwed1) + uint128 tokensOwed1 + ) ``` We see that a position only stores a single `liquidity` value, and a `tickLower` and `tickUpper` value that define the range in which the liquidity of the Position can be utilised for Swaps. @@ -109,10 +110,10 @@ const targetPrice = new Price( Be aware that the `numerator` and `denominator` parameters are ordered differently in the `Fraction` and `Price` constructor. -We have calculated our target Price but we still need to find the nearest tick to create our Position. +We have calculated our target Price but we still need to find the **nearest usable tick** to create our Position. :::info -As Positions can only start and end at initializable Ticks of the Pool, so we can only create a Range Order to a Price that exactly matches a Tick. +As Positions can only start and end at initializable Ticks of the Pool, so we can only create a Range Order to a Price that exactly matches an initializable Tick. ::: We use the `priceToClosestTick` function to find the closest tick to our targetPrice. @@ -275,6 +276,8 @@ const tokenId = decodedOutput.toNumber() We have created our Range Order Position, now we need to monitor it. +The tokenId could theoretically be too large to handle as a JS `number` but at the time of writing this guide there exist no such ids by several orders of magnitude and most certainly never will. + ## Observing the Price We need to observe the price of the Pool and withdraw our Position once the `tickCurrent` has moved across our Position. @@ -402,7 +405,7 @@ Executing a range order has certain limitations that may have become obvious dur ## Next Steps -This guide showcases everything you need to implement Range Orders on your own but only demonstrates creating a Take Profit order in `token0` to `token1` direction. +This guide showcases everything you need to implement Range Orders on your own, but only demonstrates creating a Take Profit order in `token0` to `token1` direction. Consider implementing Buy Limit orders as described in the [Range Orders concept page](../../../../concepts/protocol/range-orders.md#buy-limit-orders). This is currently the last guide in the `v3-sdk` series. Consider joining the [Uniswap Discord](https://discord.com/invite/ybKVQUWb4s) or checkout the official [Github](https://github.com/Uniswap) to learn more about the Uniswap Protocol. From c159aba663dd0b69286f9bb815e8eb31e579c8d5 Mon Sep 17 00:00:00 2001 From: Koray Koska <11356621+koraykoska@users.noreply.github.com> Date: Sun, 2 Jul 2023 18:19:51 +0100 Subject: [PATCH 22/52] feat: local development guide --- docs/sdk/v3/guides/02-local-development.md | 133 ++++++++++++++++++ docs/sdk/v3/guides/images/anvil-result.png | Bin 0 -> 140497 bytes .../guides/images/postman-chainid-result.png | Bin 0 -> 78675 bytes 3 files changed, 133 insertions(+) create mode 100644 docs/sdk/v3/guides/02-local-development.md create mode 100644 docs/sdk/v3/guides/images/anvil-result.png create mode 100644 docs/sdk/v3/guides/images/postman-chainid-result.png diff --git a/docs/sdk/v3/guides/02-local-development.md b/docs/sdk/v3/guides/02-local-development.md new file mode 100644 index 0000000000..0ea355dcbf --- /dev/null +++ b/docs/sdk/v3/guides/02-local-development.md @@ -0,0 +1,133 @@ +--- +id: local-development +title: Local Development +--- + +## Introduction + +Developing your dApps or smart contracts requires some tinkering to get a proper setup that is both a good simulation of how Mainnet will behave, +but also customizable enough to suit the needs of a development environment. + +One very common approach is to create your own custom chain offline and develop on top of it. +The issue with this approach is that if you are integrating with protocols like Uniswap or others that are on Mainnet, it's +difficult to simulate on your local chain as the smart contracts from Mainnet are not there. + +Another approach is to use a testnet like Ethereum Goerli. While most protocols (including Uniswap) have versions of their smart contracts +deployed on common testnets, there are certain behavioural differences. Not all pools that are on Mainnet are on Goerli for example. +Also, it's difficult to get enough testnet ETH to account for real testing. And without lots of testnet ETH it's even more difficult +to swap to other coins on Uniswap, if that's what you need to do in your development environment. + +This guide focuses on yet another approach to local development: Mainnet Forks. + +A Mainnet Fork is a local chain that copies the state of Ethereum Mainnet at a given block number. It then gives you access to cheat codes +like wallets with thousands of ETH and RPC URLs that you can use as drop-in replacements of real Mainnet RPCs. + +This approach combines the best of all other approaches. You have a local chain that you can manipulate to your liking +and you have real deployments of all the protocols you need to test and develop your dApp or smart contracts. + +:::info +This guide focuses on Ethereum Mainnet. But you can easily fork any other chain by simply replacing the RPC URL with +one of the network you want to use. +::: + +For this guide, the following packages are used: + +- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) +- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) +- [`ethers@5`](https://www.npmjs.com/package/ethers) + +Please note that we use ethers version 5, as this is still the most commonly used version of ethers.js. +If you use version 6, you will need to slightly change the syntax in the examples below. + +:::info +Forking a chain requires archival data and trace calls. Infura and normal geth instances are by default not archival. +You can get a free archival RPC that you can use to follow this guide and fork Mainnet you can visit [Chainnodes](https://www.chainnodes.org/). +::: + +## Using Foundry and Anvil + +There are several developer tools to fork Mainnet. +[Anvil](https://github.com/foundry-rs/foundry/blob/master/anvil/README.md) by foundry is a newcomer that's fast and easy to setup. +This guide focuses on Anvil. + +As a first step, follow the [installation guide](https://book.getfoundry.sh/getting-started/installation) in the foundry book. + +Once you have done that, you will be able to fork Mainnet straight away. Run the below command in your terminal: + +Make sure that you: + +- Replace your API Key (get one by heading to [Chainnodes](https://app.chainnodes.org/)) +- Replace the block number with a recent one, check [Etherscan](https://etherscan.io/) for that +- If you fork a non-Ethereum Mainnet chain, check [Chainlist](https://chainlist.org/) for the correct chain id and replace both occurrences in the command below + +```bash +anvil --fork-url https://mainnet.chainnodes.org/api_key --fork-block-number 17480237 --fork-chain-id 1 --chain-id 1 +``` + +Run `anvil --help` to see all available options. + +Once you have done that, you should see something like the below: + +anvil result after calling + +Your local fork of Mainnet is now running! + +And as you can see on the screenshot above, anvil prints a bunch of private keys that are loaded with 10k ETH each. +We will use them going forward to send transactions, including swaps on Uniswap pools. + +:::warning +Security consideration: This is a fork of Mainnet and the same chain id is used. You have no replay protection to Ethereum Mainnet. So you need to 1: Never use the anvil private keys on a real chain or send funds to it +(they are leaked everywhere) and 2: Not send any transactions to your local fork chain with accounts that you use on Ethereum Mainnet or other real chains. +::: + +If you scroll down in your terminal, near the bottom of the anvil logs you will find your RPC URL. +If you haven't changed any configs, it should be `127.0.0.1:8545`. +This is the RPC URL that you can now use as a drop-in replacement everywhere in your development environment, and interact with it +as if it was real Ethereum Mainnet. You can use the http provider `http://127.0.0.1:8545` as well as the Websocket provider `ws://127.0.0.1:8545`. + +You can now make a sample RPC request to your http provider using [Postman](https://www.postman.com/) using the below: + +POST `http://127.0.0.1:8545` + +Body: + +```JSON +{ + "jsonrpc": "2.0", + "method": "eth_chainId", + "params": [], + "id": 1 +} +``` + +The result should look like the below (see image below as well): + +```JSON +{ + "jsonrpc": "2.0", + "id": 1, + "result": "0x1" +} +``` + +anvil result after calling + +As you can see, the chain id is `1`, just like on Mainnet! + +## Using your Mainnet Fork + +Now that you have a running Mainnet Fork, you will be able to use it everywhere in your development setup. +Using one of the private keys provided by anvil, you have access to enough ETH to do endless swaps and smart contract calls. + +If you need any other token, you now have the flexibility of swapping your ETH to any token that has a pool deployed on Mainnet. +You basically take your fake ETH and swap it to the token you need. + +Check out one of the [guides about swapping](./swaps/02-trading.md) and replace the RPC URL with your local anvil HTTP link as above. + +## Next Steps + +Using the above you are fully equipped to continue following the guides about how to use Uniswap while testing everything locally before +going on Mainnet. You will also be able to reuse what you have learned when you develop your own protocols or dApps. + +You can also continue tapping into other developer tools that make smart contract development easier like [forge](https://github.com/foundry-rs/foundry/tree/master/forge) from foundry. +To read more about foundry and their developer tooling, visit their [Github](https://github.com/foundry-rs/foundry) or the [foundry book](https://book.getfoundry.sh/). diff --git a/docs/sdk/v3/guides/images/anvil-result.png b/docs/sdk/v3/guides/images/anvil-result.png new file mode 100644 index 0000000000000000000000000000000000000000..669539bbd7edea9bfd76ffc06ee102b4d96ce3cd GIT binary patch literal 140497 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

ad@RVd9sE$FUe-m9vBU(ol3WW1B;b*e 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આ}|U=mu9Ptnekyk{9IPLn*7I~&k04j_u5 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 z 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 literal 0 HcmV?d00001 From 9a11ce003fb720eb74f804af469f735d065b93d4 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Mon, 3 Jul 2023 14:43:40 +0100 Subject: [PATCH 23/52] Add graph warning to pool data guide --- docs/sdk/v3/guides/advanced/02-pool-data.md | 7 ++++++- docs/sdk/v3/guides/advanced/05-range-orders.md | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/sdk/v3/guides/advanced/02-pool-data.md b/docs/sdk/v3/guides/advanced/02-pool-data.md index 1680550b7c..59101138fc 100644 --- a/docs/sdk/v3/guides/advanced/02-pool-data.md +++ b/docs/sdk/v3/guides/advanced/02-pool-data.md @@ -160,7 +160,12 @@ const usdcWethPool = new Pool( ) ``` -With this fully initialized Pool, we can calculate swaps on it offchain, without the need to make expensive RPC calls. +With this fully initialized Pool, we can display all the datapoints we fetched, without the need to make thousands of expensive RPC calls. + +```warning +The Uniswap subgraphs do not handle block reorgs correctly, resulting in (slightly) incorrect data for Entities where historic reorgs are relevant to their state, e.g. Pools, Positions. +Do not use theGraph for calculations where exact results are needed, but rather for visualization. +``` ## Next Steps diff --git a/docs/sdk/v3/guides/advanced/05-range-orders.md b/docs/sdk/v3/guides/advanced/05-range-orders.md index 9cf35a7005..daa24f8be2 100644 --- a/docs/sdk/v3/guides/advanced/05-range-orders.md +++ b/docs/sdk/v3/guides/advanced/05-range-orders.md @@ -271,12 +271,12 @@ const decodedOutput = ethers.utils.defaultAbiCoder.decode( mintCallOutput )[0] -const tokenId = decodedOutput.toNumber() +const tokenId = decodedOutput.toString() ``` We have created our Range Order Position, now we need to monitor it. -The tokenId could theoretically be too large to handle as a JS `number` but at the time of writing this guide there exist no such ids by several orders of magnitude and most certainly never will. +The decodedOutput we get from the AbiCoder is a `Bignumber` so we need to cast it to a string to use it with the SDK. ## Observing the Price From 49dbd7c5deed4e65e3222f3316c37475cc359ce4 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Mon, 3 Jul 2023 14:46:08 +0100 Subject: [PATCH 24/52] Fix formatting of warning --- docs/sdk/v3/guides/advanced/02-pool-data.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sdk/v3/guides/advanced/02-pool-data.md b/docs/sdk/v3/guides/advanced/02-pool-data.md index 59101138fc..2a097ba209 100644 --- a/docs/sdk/v3/guides/advanced/02-pool-data.md +++ b/docs/sdk/v3/guides/advanced/02-pool-data.md @@ -162,10 +162,10 @@ const usdcWethPool = new Pool( With this fully initialized Pool, we can display all the datapoints we fetched, without the need to make thousands of expensive RPC calls. -```warning +:::warning The Uniswap subgraphs do not handle block reorgs correctly, resulting in (slightly) incorrect data for Entities where historic reorgs are relevant to their state, e.g. Pools, Positions. Do not use theGraph for calculations where exact results are needed, but rather for visualization. -``` +::: ## Next Steps From 35e98f0284422abdf01f06f6f2e9b2708459a55c Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Mon, 24 Jul 2023 18:26:33 +0100 Subject: [PATCH 25/52] Change pool data fetching to multicall and move graph section to active liquidity guide --- docs/sdk/v3/guides/advanced/02-pool-data.md | 196 +++++++++++++----- .../v3/guides/advanced/03-active-liquidity.md | 61 +++++- 2 files changed, 201 insertions(+), 56 deletions(-) diff --git a/docs/sdk/v3/guides/advanced/02-pool-data.md b/docs/sdk/v3/guides/advanced/02-pool-data.md index 2a097ba209..c4f358ab31 100644 --- a/docs/sdk/v3/guides/advanced/02-pool-data.md +++ b/docs/sdk/v3/guides/advanced/02-pool-data.md @@ -11,14 +11,15 @@ This guide will cover how to initialize a Pool with full tick data to allow offc If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](./01-background.md) page! ::: -In this example we will use **ethers JS** and **The Graph** to construct a `Pool` object that we can use in the following guides. We will also [Axios](https://axios-http.com/docs/intro) for requests, but any http client is fine. +In this example we will use **ethers JS** and **ethers-multicall** to construct a `Pool` object that we can use in the following guides. This guide will **cover**: 1. Computing the Pool's address 2. Referencing the Pool contract and fetching metadata -3. Using the V3 subgraph to fetch Tick data -4. Constructing the Pool object +3. Fetching the positions of all initialized Ticks with multicall +4. Fetching all ticks by their indices with a multicall +5. Constructing the Pool object At the end of the guide, we will have created a `Pool` Object that accurately represents the state of a V3 pool at the time we fetched it. @@ -27,7 +28,11 @@ For this guide, the following Uniswap packages are used: - [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) - [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) -The core code of this guide can be found in [`pool.ts`](https://github.com/Uniswap/examples/tree/main/v3-sdk/pool-data/src/lib/pool.ts) +We will also use the `ethers-multicall` npm package: + +- [`ethers-multicall`](https://www.npmjs.com/package/ethers-multicall) + +The core code of this guide can be found in [`fetcher.ts`](https://github.com/Uniswap/examples/tree/main/v3-sdk/multicall/src/libs/fetcher.ts) ## Computing the Pool's deployment address @@ -97,54 +102,156 @@ For our use case, we only need the `sqrtPriceX96` and the currently active `tick V3 pools use ticks to [concentrate liquidity](../../../concepts/protocol/concentrated-liquidity.md) in price ranges and allow for better pricing of trades. Even though most Pools only have a couple of **initialized ticks**, it is possible that a pools liquidity is defined by thousands of **initialized ticks**. -In that case, it can be very expensive or slow to get all of them with RPC calls. +In that case, it can be very expensive or slow to get all of them with normal RPC calls. + +### Multicall + +Multicall contracts **aggregate results** from multiple contract calls and therefore allow sending multiple contract calls in **one RPC request**. +This can improve the **speed** of fetching large amounts of data significantly and ensures that the data fetched is all from the **same block**. -To fetch all ticks of the **USDC - WETH Pool**, we will use the [Uniswap V3 graph](../../../../api/subgraph/overview.md). -To construct a `Tick` for the SDK, we need the **tickIdx**, the **liquidityGross** and the **liquidityNet**. +We will use the Multicall2 contract by MakerDAO. +We use the `ethers-muticall` npm package to easily interact with the Contract. -We define our GraphQL query and [send a POST request](https://axios-http.com/docs/post_example) to the V3 subgraph API endpoint: +### Fetching the positions of initialized Ticks + +Uniswap V3 Pools store **bitmaps**, also called *words*, that represent the state of **256 initializable ticks** at a time. +The value at a bit of a word is 1 if the tick at this index is initialized and 0 if it isn't. +We can calculate the positions of initialized ticks from the **words** of the Pool. + +All ticks of Uniswap V3 pools are between the indices `-887272` and `887272`. +We can calculate the minimum and maximum word from these indices and the Pool's tickSpacing: ```typescript -axios.post( - "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3", - {"query": `{ ticks( - where: {poolAddress: "${poolAddress.toLowerCase()}", liquidityNet_not: "0"} - first: 1000, - skip: ${skip}, - orderBy: tickIdx, - orderDirection: asc - ) { - tickIdx - liquidityGross - liquidityNet - } - }` - }, - { - headers: { - "Content-Type": "application/json" - } +function tickToWord(tick: number): number { + let compressed = Math.floor(tick / tickSpacing) + if (tick < 0 && tick % tickSpacing !== 0) { + compressed -= 1 + } + return tick >> 8 +} + +const minWord = tickToWord(-887272) +const maxWord = tickToWord(887272) +``` + +Ticks can only be initialized at indices that are **divisible by the tickSpacing**. +One word contains 256 ticks, so we can compress the ticks by right shifting 8 bit. + +Knowing the word indices, we can now fetch them from the Pool using multicall and the `tickBitmap` read call. + +First we initialize our multicall providers and Pool Contract: + +```typescript +import { ethers } from 'ethers' +import { Contract, Provider } from 'ethers-multicall' + +const ethersProvider = new ethers.providers.JsonRpcProvider("...rpcUrl") +const multicallProvider = new Provider(ethersProvider) +await multicallProvider.init() + +const poolContract = new Contract(poolAddress, IUniswapV3PoolABI.abi) +``` + +The `multicallProvider` creates the multicall request and sends it via the ethers Provider. + +Next we loop through all possible word indices and add a `tickBitmap` call for each: + +```typescript +let calls: any[] = [] +let wordPosIndices: number[] = [] +for (let i = minWord; i <= maxWord; i++) { + wordPosIndices.push(i) + calls.push(poolContract.tickBitmap(i)) +} +``` + +We also keep track of the word indices to be able to loop through them in the same order we added the calls to the array. + +We use the `multicallProvider.all()` function to send a multicall and map the results: + +```typescript +const results: bigint[] = (await multicallProvider.all(calls)).map( + (ethersResponse) => { + return BigInt(ethersResponse.toString()) + } + ) +``` + +Lastly, we check which ticks are initialized in the words we fetched and calculate the **tick position** from the **word index** and the **tickSpacing** of the pool. + +We check if a tick is **initialized** inside the word by shifting it to the position and performing a bitwise AND operation: + +```typescript +const bit = 1n +const initialized = (bitmap & (bit << BigInt(i))) !== 0n +``` +If the tick is **initialized**, we revert the compression from tick to word we made earlier by multiplying the word index with 256, which is the same as left shifting by 8 bit, adding the position we are currently at, and multiplying with the tickSpacing: + +```typescript +const tickIndex = (ind * 256 + i) * tickSpacing +``` + +The whole loop looks like this: + +```typescript +const tickIndices: number[] = [] + + for (let j = 0; j < wordPosIndices.length; j++) { + const ind = wordPosIndices[j] + const bitmap = results[j] + + if (bitmap !== 0n) { + for (let i = 0; i < 256; i++) { + const bit = 1n + const initialized = (bitmap & (bit << BigInt(i))) !== 0n + if (initialized) { + const tickIndex = (ind * 256 + i) * tickSpacing + tickIndices.push(tickIndex) } - ) + } + } + } ``` -We only fetch the ticks that **have liquidity**, and we convert the poolAddress to **lower case** for the subgraph to work with. To make sure the Ticks are ordered correctly, we also define the **order direction** in the query. +We now have an array containing the indices of all initialized Ticks. -To create our Pool, we need to map the raw data we received from the subgraph to `Tick` objects that the SDK can work with: +### Fetching all Ticks by their indices + +We use the multicallProvider again to execute an aggregated read call for all tick indices. +We create an array of call Promises again and use `.all()` to make our multicall: + +```typescript +const calls: any[] = [] + +for (const index of tickIndices) { + calls.push(poolContract.ticks(index)) +} + +const results = await multicallProvider.all(calls) +``` + +Again, the order of the results array is the same as the elements in **tickIndices**. + +We are able to combine the **tickIndices** and **results** array to create an array of `Tick` objects: ```typescript -const sdkTicks = ticks.map((graphTick: GraphTick) => { - return new Tick({ - index: +graphTick.tickIdx, - liquidityGross: graphTick.liquidityGross, - liquidityNet: graphTick.liquidityNet - }) +const allTicks: Tick[] = [] + + for (let i = 0; i < tickIndices.length; i++) { + const index = tickIndices[i] + const ethersResponse = results[i] + const tick = new Tick({ + index, + liquidityGross: JSBI.BigInt(ethersResponse.liquidityGross.toString()), + liquidityNet: JSBI.BigInt(ethersResponse.liquidityNet.toString()), }) + allTicks.push(tick) + } ``` -:::note -GraphQL is only able to fetch 1000 records at a time. If a pool has more than 1000 initialized ticks, multiple calls are necessary to get all of them. -::: +We need to parse the response from our RPC provider to JSBI values that the v3-sdk can work with. + +## Constructing the Pool We have everything to construct our `Pool` now: @@ -156,17 +263,12 @@ const usdcWethPool = new Pool( slot0.sqrtPriceX96, liquidity, slot0.tick, - sdkTicks + allTicks ) ``` -With this fully initialized Pool, we can display all the datapoints we fetched, without the need to make thousands of expensive RPC calls. - -:::warning -The Uniswap subgraphs do not handle block reorgs correctly, resulting in (slightly) incorrect data for Entities where historic reorgs are relevant to their state, e.g. Pools, Positions. -Do not use theGraph for calculations where exact results are needed, but rather for visualization. -::: +With this fully initialized Pool, we can make accurate offchain calculations. ## Next Steps -Now that you are familiar with using TheGraph, continue your journey with the [next example](./03-active-liquidity.md) on visualizing the Liquidity density of a pool. +Now that you are familiar with fetching Pool data, continue your journey with the [next example](./03-active-liquidity.md) on visualizing the Liquidity density of a pool. diff --git a/docs/sdk/v3/guides/advanced/03-active-liquidity.md b/docs/sdk/v3/guides/advanced/03-active-liquidity.md index 06821c3b7e..9129888202 100644 --- a/docs/sdk/v3/guides/advanced/03-active-liquidity.md +++ b/docs/sdk/v3/guides/advanced/03-active-liquidity.md @@ -11,7 +11,7 @@ This guide will cover how to fetch and compute the active liquidity in the speci If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](./01-background.md) page! ::: -In this guide, we will use the Tick data we fetched from the V3 subgraph in the previous guide and compute the active liquidity our Pool can use at each Tick. We then use `recharts` to draw a chart that visualizes our Pool's liqudity density. +In this guide, we will use the V3 subgraph to fetch all ticks from **theGraph** and compute the active liquidity our Pool can use at each Tick. We then use `recharts` to draw a chart that visualizes our Pool's liqudity density. This guide will cover: @@ -44,16 +44,55 @@ This is what the **Initialized Ticks** of a Pool represent - they are a represen When entering or leaving a position, its liquidity is added or removed from the **active liquidity available** for a Swap. The initialized Ticks store this change in available liquidity in the `liquidityNet` field. -The change is always stored in relation to the currently active Tick - the current price. +The change is always stored in relation to the currently active Tick - the current price. When the price crosses an initialized Tick, it gets updated and liqudity that was previously added when crossing the Tick would now be removed and vice versa. -We already fetched the initialized Ticks of our Pool in the [previous guide](./02-pool-data.md). The format we got from the Graph is: +### Fetching initialized Ticks + +To demonstrate another way + +To fetch all ticks of our Pool, we will use the [Uniswap V3 graph](../../../../api/subgraph/overview.md). +To visualize active liquidity, we need the **tickIdx**, the **liquidityGross** and the **liquidityNet**. + +We define our GraphQL query and [send a POST request](https://axios-http.com/docs/post_example) to the V3 subgraph API endpoint: + +```typescript +axios.post( + "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3", + {"query": `{ ticks( + where: {poolAddress: "${poolAddress.toLowerCase()}", liquidityNet_not: "0"} + first: 1000, + skip: ${skip}, + orderBy: tickIdx, + orderDirection: asc + ) { + tickIdx + liquidityGross + liquidityNet + } + }` + }, + { + headers: { + "Content-Type": "application/json" + } + } + ) +``` + +We only fetch the ticks that **have liquidity**, and we convert the poolAddress to **lower case** for the subgraph to work with. To make sure the Ticks are ordered correctly, we also define the **order direction** in the query. + +:::note +GraphQL is only able to fetch 1000 records at a time. If a pool has more than 1000 initialized ticks, multiple calls are necessary to get all of them. +::: + +The ticks we got from **theGraph** have this format: ```typescript interface GraphTick { - tickIdx: string - liquidityGross: string - liquidityNet: string + tickIdx: string + liquidityGross: string + liquidityNet: string } ``` @@ -61,7 +100,7 @@ interface GraphTick { The current Tick of the Pool represents the **current Price** after the last swap. Considering that the initialized Ticks only represent positions, we see that it is not necessarily one of the initialized Ticks but can be at any point in between them. -The active liqudity at the current Price is also stored in the smart contract - we already fetched it with the `liquidity` function. +The active liqudity at the current Price is also stored in the smart contract - we already fetched it with the `liquidity` function in the [previous guide](./02-pool-data.md). ### Tickspacing @@ -69,12 +108,16 @@ Only the Ticks with indices that are dividable with 0 remainder by the tickspaci The Tickspacing of the Pool is dependent on the Fee Tier. Pools with lower fees are meant to be used for more stable Token Pairs and allow for more granularity in where LPs position their liquidity. -We can get the `tickSpacing`from our pool: +We can get the `tickSpacing` from the `TICK_SPACINGS` enum exposed by the `v3-sdk`: ```typescript -const tickSpacing = pool.tickSpacing +import { TICK_SPACINGS } + +const tickSpacing = TICK_SPACINGS[fee] ``` +Alternatively, if we have already constructed a `Pool` object, we couild just call `Pool.tickSpacing()`. + ### Putting it all together For the purpose of visualizing the liquidity density of the Pool, it rarely makes sense to display the full Tick Range of the Pool, as the vast majority of liquidity will be focused in a narrow price range. From bf8cace97aac5748f533b019bb90af4d6497b603 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Thu, 27 Jul 2023 20:18:32 +0100 Subject: [PATCH 26/52] WIP: Rework based on Foundation feedback --- docs/sdk/v3/guides/advanced/02-pool-data.md | 4 +- .../v3/guides/advanced/03-active-liquidity.md | 2 +- .../v3/guides/liquidity/01-position-data.md | 140 ++---------------- .../guides/liquidity/02-minting-position.md | 14 +- .../guides/liquidity/03-fetching-positions.md | 127 ++++++++++++++++ ...g-position.md => 04-modifying-position.md} | 0 ...llecting-fees.md => 05-collecting-fees.md} | 0 ...uidity.md => 06-swap-and-add-liquidity.md} | 0 docs/sdk/v3/guides/swaps/01-quoting.md | 48 ++++-- docs/sdk/v3/guides/swaps/02-trading.md | 128 ++++++++++++++-- docs/sdk/v3/guides/swaps/03-routing.md | 32 +++- 11 files changed, 339 insertions(+), 156 deletions(-) create mode 100644 docs/sdk/v3/guides/liquidity/03-fetching-positions.md rename docs/sdk/v3/guides/liquidity/{03-modifying-position.md => 04-modifying-position.md} (100%) rename docs/sdk/v3/guides/liquidity/{04-collecting-fees.md => 05-collecting-fees.md} (100%) rename docs/sdk/v3/guides/liquidity/{05-swap-and-add-liquidity.md => 06-swap-and-add-liquidity.md} (100%) diff --git a/docs/sdk/v3/guides/advanced/02-pool-data.md b/docs/sdk/v3/guides/advanced/02-pool-data.md index c4f358ab31..8f2f979228 100644 --- a/docs/sdk/v3/guides/advanced/02-pool-data.md +++ b/docs/sdk/v3/guides/advanced/02-pool-data.md @@ -51,7 +51,7 @@ const poolAddress = Pool.getAddress( ) ``` -Uniswap V3 allows different Fee tiers when deploying a pool, so multiple pools can exist for each pair of tokens. +Uniswap V3 allows different Fee tiers when deploying a pool, so multiple pools can exist for each pair of tokens. FeeAmount.LOW means the pool has a swap fee of 0.05%. ## Creating a Pool Contract instance and fetching metadata @@ -100,7 +100,7 @@ For our use case, we only need the `sqrtPriceX96` and the currently active `tick ## Fetching all Ticks -V3 pools use ticks to [concentrate liquidity](../../../concepts/protocol/concentrated-liquidity.md) in price ranges and allow for better pricing of trades. +V3 pools use ticks to [concentrate liquidity](../../../../concepts/protocol/concentrated-liquidity.md) in price ranges and allow for better pricing of trades. Even though most Pools only have a couple of **initialized ticks**, it is possible that a pools liquidity is defined by thousands of **initialized ticks**. In that case, it can be very expensive or slow to get all of them with normal RPC calls. diff --git a/docs/sdk/v3/guides/advanced/03-active-liquidity.md b/docs/sdk/v3/guides/advanced/03-active-liquidity.md index 9129888202..df2470ee6e 100644 --- a/docs/sdk/v3/guides/advanced/03-active-liquidity.md +++ b/docs/sdk/v3/guides/advanced/03-active-liquidity.md @@ -116,7 +116,7 @@ import { TICK_SPACINGS } const tickSpacing = TICK_SPACINGS[fee] ``` -Alternatively, if we have already constructed a `Pool` object, we couild just call `Pool.tickSpacing()`. +Alternatively, if we have already constructed a `Pool` object, we could just call `Pool.tickSpacing()`. ### Putting it all together diff --git a/docs/sdk/v3/guides/liquidity/01-position-data.md b/docs/sdk/v3/guides/liquidity/01-position-data.md index 55477da566..86adc92f93 100644 --- a/docs/sdk/v3/guides/liquidity/01-position-data.md +++ b/docs/sdk/v3/guides/liquidity/01-position-data.md @@ -21,10 +21,22 @@ For this guide, the following Uniswap packages are used: The code mentioned in this guide can be found across the [minting Position](https://github.com/Uniswap/examples/blob/main/v3-sdk/minting-position/src), [collecting Fees](https://github.com/Uniswap/examples/blob/main/v3-sdk/collecting-fees/src), [modifying positions](https://github.com/Uniswap/examples/blob/d34a53412dbf905802da2249391788a225719bb8/v3-sdk/modifying-position/src) and [swap and add liquidity](https://github.com/Uniswap/examples/blob/main/v3-sdk/swap-and-add-liquidity/src) examples. +## Prerequisites + +To understand what Positions are, we need to understand some underlying concepts of the Uniswap protocol. + +### Concentrated liquidity + +Uniswap V3 Pools + +### Ticks + +### Liquidity Positions + ## Position class -The **sdk** provides a `Position` class used to create local representations of an onchain position. -It is used to create the calldata for onchain calls to mint or modify an onchain position. +The **sdk** provides a [`Position`](https://github.com/Uniswap/v3-sdk/blob/main/src/entities/position.ts) class used to create local representations of an onchain position. +It is used to create the calldata for onchain calls to mint or modify an onchain position. There are four ways to construct a position. @@ -100,8 +112,8 @@ const singleSidePositionToken1 = Position.fromAmount1({ }) ``` -These last two functions calculate a position at the given tick range given the amount of `token0` or `token1` and an unlimited amount of the other Token. -For example, if a tick range where the ratio between `token0` and `token1` is 1 : 2 is defined by `tickLower` and `tickUpper`, the position would be created with that ratio. +These last two functions calculate a position at the given tick range given the amount of `token0` or `token1`. The amount of the second token is calculated from the ratio of the tokens inside the tick range and the amount of token one. + A create transaction would then fail if the wallet doesn't hold enough `token1` or the Contract is not given the necessary **Transfer Approval**. All of these functions take an Object with **named values** as a call parameter. The amount and liquidity values are of type `BigIntish` which accepts `number`, `string` and `JSBI`. @@ -162,126 +174,6 @@ const { calldata, value } = NonfungiblePositionManager.collectCallParameters(collectOptions) ``` -## Fetching Positions - -The [NonfungiblePositionManager Contract](../../../../contracts/v3/reference/periphery/NonfungiblePositionManager.md) can be used to create Positions, as well as get information on existing Positions. - -In this section we will **fetch all Positions** for an address. - -### Creating an ethers Contract - -We use **ethersJS** to interact with the NonfungiblePositionManager Contract. Let's create an ethers Contract: - -```typescript -import { ethers } from 'ethers' -import INONFUNGIBLE_POSITION_MANAGER from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json' - -const provider = new ethers.providers.JsonRpcProvider(rpcUrl) - -const nfpmContract = new ethers.Contract( - NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, - INONFUNGIBLE_POSITION_MANAGER.abi, - provider -) -``` - -We get the Contract ABI from the 'v3-periphery` package and the contract address from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md) - -### Fetching the Position Ids - -We want to fetch all Position Ids for our address. We first fetch the number of positions and then the ids by their indices. - -We fetch the number of positions using the `balanceOf` read call: - -```typescript - -const numPositions = await nfpmContract.balanceOf(address) -``` - -Next we iterate over the number of positions and fetch the ids: - -```typescript -const calls = [] - -for (let i = 0; i < numPositions; i++) { - calls.push( - nfpmContract.tokenOfOwnerByIndex(address, i) - ) -} - -const positionIds = await Promise.all(calls) -``` - -### Fetching the Position Info - -Now that we have the ids of the Positions associated with our address, we can fetch the position info using the `positions` function. - -The solidity function returns a lot of values describing the Position: - -```solidity -function positions( - uint256 tokenId - ) external view returns ( - uint96 nonce, - address operator, - address token0, - address token1, - uint24 fee, - int24 tickLower, - int24 tickUpper, - uint128 liquidity, - uint256 feeGrowthInside0LastX128, - uint256 feeGrowthInside1LastX128, - uint128 tokensOwed0, - uint128 tokensOwed1 - ) -``` - -In this example we only care about values needed to interact with positions, so we create an Interface `PositionInfo`: - -```typescript -interface PositionInfo { - tickLower: number - tickUpper: number - liquidity: JSBI - feeGrowthInside0LastX128: JSBI - feeGrowthInside1LastX128: JSBI - tokensOwed0: JSBI - tokensOwed1: JSBI -} -``` - -We fetch the Position data with `positions`: - -```typescript -const positionCalls = [] - -for (let id of positionIds) { - positionCalls.push( - nfpmContract.positions(id) - ) -} - -const callResponses = await Promise.all(positionCalls) -``` - -Finally, we map the RPC response to our interface: - -```typescript -const positionInfos = callResponses.map((position) => { - return { - tickLower: position.tickLower, - tickUpper: position.tickUpper, - liquidity: JSBI.BigInt(position.liquidity), - feeGrowthInside0LastX128: JSBI.BigInt(position.feeGrowthInside0LastX128), - feeGrowthInside1LastX128: JSBI.BigInt(position.feeGrowthInside1LastX128), - tokensOwed0: JSBI.BigInt(position.tokensOwed0), - tokensOwed1: JSBI.BigInt(position.tokensOwed1), - } -}) -``` - -We now have an array containing PositionInfo for all positions that our address holds. ## Next steps diff --git a/docs/sdk/v3/guides/liquidity/02-minting-position.md b/docs/sdk/v3/guides/liquidity/02-minting-position.md index 89e69d10eb..2cac516da5 100644 --- a/docs/sdk/v3/guides/liquidity/02-minting-position.md +++ b/docs/sdk/v3/guides/liquidity/02-minting-position.md @@ -7,7 +7,7 @@ title: Minting a Position This guide will cover how to create (or mint) a liquidity position on the Uniswap V3 protocol. It is based on the [minting a position code example](https://github.com/Uniswap/examples/tree/main/v3-sdk/minting-position), found in the Uniswap code examples [repository](https://github.com/Uniswap/examples). -To run this example, check out the examples's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/minting-posotion/README.md) and follow the setup instructions. +To run this example, check out the examples's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/minting-position/README.md) and follow the setup instructions. :::info If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](../01-background.md) page! @@ -69,13 +69,15 @@ async function getTokenTransferApproval(address: string, amount: BigNumber) { ``` We can get the Contract address for the NonfungiblePositionManager from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). +For Ethereum mainnet or a local fork of mainnet, we see that the contract address is `0xC36442b4a4522E871399CD717aBDD847Ab11FE88`. +In our example, this is defined in the [`constants.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/minting-position/src/libs/constants.ts) file. ## Creating an instance of a `Pool` Having approved the transfer of our tokens, we now need to get data about the pool for which we will provide liquidity, in order to instantiate a Pool class. -To start, we compute our Pool's address by using a helper function and passing in the unique identifiers of a Pool - the **two tokens** and the Pool **fee**. -The **fee** input parameter represents the swap fee that is distributed to all in range liquidity at the time of the swap: +To start, we compute our Pool's address by using a helper function and passing in the unique identifiers of a Pool - the **two tokens** and the Pool **fee**. +The **fee** input parameter represents the swap fee that is distributed to all in range liquidity at the time of the swap. ```typescript import { computePoolAddress, FeeAmount } from '@uniswap/v3-sdk' @@ -94,9 +96,11 @@ const currentPoolAddress = computePoolAddress({ }) ``` -Again, we can get the factory contract address from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). +Again, we can get the factory contract address from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). +For Ethereum mainnet, or a local fork of mainnet, it is `0x1F98431c8aD98523631AE4a59f267346ea31F984`. +In our example, it is defined in [`constants.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/minting-position/src/libs/constants.ts) -Then, we get the Pool's data by creating a reference to the Pool's smart contract and accessing its methods: +Then, we get the Pool's data by creating a reference to the Pool's smart contract and accessing its methods, very similar to what we did in the [Quoting guide](../swaps/01-quoting.md#referencing-the-pool-contract-and-fetching-metadata): ```typescript import IUniswapV3PoolABI from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json' diff --git a/docs/sdk/v3/guides/liquidity/03-fetching-positions.md b/docs/sdk/v3/guides/liquidity/03-fetching-positions.md new file mode 100644 index 0000000000..39542b5c66 --- /dev/null +++ b/docs/sdk/v3/guides/liquidity/03-fetching-positions.md @@ -0,0 +1,127 @@ +--- +id: fetching-positions +title: Fetching Positions +--- + +# Introduction + +## Fetching Positions + +The [NonfungiblePositionManager Contract](../../../../contracts/v3/reference/periphery/NonfungiblePositionManager.md) can be used to create Positions, as well as get information on existing Positions. + +In this section we will **fetch all Positions** for an address. + +### Creating an ethers Contract + +We use **ethersJS** to interact with the NonfungiblePositionManager Contract. Let's create an ethers Contract: + +```typescript +import { ethers } from 'ethers' +import INONFUNGIBLE_POSITION_MANAGER from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json' + +const provider = new ethers.providers.JsonRpcProvider(rpcUrl) + +const nfpmContract = new ethers.Contract( + NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, + INONFUNGIBLE_POSITION_MANAGER.abi, + provider +) +``` + +We get the Contract ABI from the 'v3-periphery` package and the contract address from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md) + +### Fetching the Position Ids + +We want to fetch all Position Ids for our address. We first fetch the number of positions and then the ids by their indices. + +We fetch the number of positions using the `balanceOf` read call: + +```typescript + +const numPositions = await nfpmContract.balanceOf(address) +``` + +Next we iterate over the number of positions and fetch the ids: + +```typescript +const calls = [] + +for (let i = 0; i < numPositions; i++) { + calls.push( + nfpmContract.tokenOfOwnerByIndex(address, i) + ) +} + +const positionIds = await Promise.all(calls) +``` + +### Fetching the Position Info + +Now that we have the ids of the Positions associated with our address, we can fetch the position info using the `positions` function. + +The solidity function returns a lot of values describing the Position: + +```solidity +function positions( + uint256 tokenId + ) external view returns ( + uint96 nonce, + address operator, + address token0, + address token1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ) +``` + +In this example we only care about values needed to interact with positions, so we create an Interface `PositionInfo`: + +```typescript +interface PositionInfo { + tickLower: number + tickUpper: number + liquidity: JSBI + feeGrowthInside0LastX128: JSBI + feeGrowthInside1LastX128: JSBI + tokensOwed0: JSBI + tokensOwed1: JSBI +} +``` + +We fetch the Position data with `positions`: + +```typescript +const positionCalls = [] + +for (let id of positionIds) { + positionCalls.push( + nfpmContract.positions(id) + ) +} + +const callResponses = await Promise.all(positionCalls) +``` + +Finally, we map the RPC response to our interface: + +```typescript +const positionInfos = callResponses.map((position) => { + return { + tickLower: position.tickLower, + tickUpper: position.tickUpper, + liquidity: JSBI.BigInt(position.liquidity), + feeGrowthInside0LastX128: JSBI.BigInt(position.feeGrowthInside0LastX128), + feeGrowthInside1LastX128: JSBI.BigInt(position.feeGrowthInside1LastX128), + tokensOwed0: JSBI.BigInt(position.tokensOwed0), + tokensOwed1: JSBI.BigInt(position.tokensOwed1), + } +}) +``` + +We now have an array containing PositionInfo for all positions that our address holds. diff --git a/docs/sdk/v3/guides/liquidity/03-modifying-position.md b/docs/sdk/v3/guides/liquidity/04-modifying-position.md similarity index 100% rename from docs/sdk/v3/guides/liquidity/03-modifying-position.md rename to docs/sdk/v3/guides/liquidity/04-modifying-position.md diff --git a/docs/sdk/v3/guides/liquidity/04-collecting-fees.md b/docs/sdk/v3/guides/liquidity/05-collecting-fees.md similarity index 100% rename from docs/sdk/v3/guides/liquidity/04-collecting-fees.md rename to docs/sdk/v3/guides/liquidity/05-collecting-fees.md diff --git a/docs/sdk/v3/guides/liquidity/05-swap-and-add-liquidity.md b/docs/sdk/v3/guides/liquidity/06-swap-and-add-liquidity.md similarity index 100% rename from docs/sdk/v3/guides/liquidity/05-swap-and-add-liquidity.md rename to docs/sdk/v3/guides/liquidity/06-swap-and-add-liquidity.md diff --git a/docs/sdk/v3/guides/swaps/01-quoting.md b/docs/sdk/v3/guides/swaps/01-quoting.md index 94b338539e..f858bacd25 100644 --- a/docs/sdk/v3/guides/swaps/01-quoting.md +++ b/docs/sdk/v3/guides/swaps/01-quoting.md @@ -5,7 +5,7 @@ title: Getting a Quote ## Introduction -This guide will cover how to get the current quotes for any token pair on the Uniswap protocol. It is based on the [Quoting code example](https://github.com/Uniswap/examples/tree/main/v3-sdk/quoting), found in the Uniswap code examples [repository](https://github.com/Uniswap/examples). To run this example, check out the examples's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/minting-position/README.md) and follow the setup instructions. +This guide will cover how to get the current quotes for any token pair on the Uniswap protocol. It is based on the [Quoting code example](https://github.com/Uniswap/examples/tree/main/v3-sdk/quoting), found in the Uniswap code examples [repository](https://github.com/Uniswap/examples). To run this example, check out the examples's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/quoting/README.md) and follow the setup instructions. :::info If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](../01-background.md) page! @@ -54,9 +54,33 @@ interface ExampleConfig { export const CurrentConfig: ExampleConfig = {...} ``` +The default config of the example uses a local fork of mainnet. If you haven't already, check out our [local development guide](../02-local-development.md). +To change the rpc endpoint or the Pool used, edit the [`Currentconfig`](https://github.com/Uniswap/examples/blob/main/v3-sdk/quoting/src/config.ts#L21). +To connect to mainnet directly, set the `mainnet` field in the config: + +```typescript +export const CurrentConfig: ExampleConfig = { + rpc: { + local: 'http://localhost:8545', + mainnet: 'https://mainnet.infura.io/v3/0ac57a06f2994538829c14745750d721', + }, + tokens: { + in: USDC_TOKEN, + amountIn: 1000, + out: WETH_TOKEN, + poolFee: FeeAmount.MEDIUM, + }, +} +``` + +The pool used is defined by a pair of tokens in [`constants.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/quoting/src/libs/constants.ts#L14). +You can also change these two tokens and the fee of the pool in the config, just make sure a Pool actually exists for your configuration. +Check out the top pools on [Uniswap info](https://info.uniswap.org/#/pools). + ## Computing the Pool's deployment address To interact with the **USDC - WETH** Pool contract, we first need to compute its deployment address. +If you haven't worked directly with smart contracts yet, check out this [guide](https://docs.alchemy.com/docs/smart-contract-basics) from Alchemy. The SDK provides a utility method for that: ```typescript @@ -72,7 +96,7 @@ const currentPoolAddress = computePoolAddress({ Since each *Uniswap V3 Pool* is uniquely identified by 3 characteristics (token in, token out, fee), we use those in combination with the address of the *PoolFactory* contract to compute the address of the **USDC - ETH** Pool. -These parameters have already been defined in our configuration file: +These parameters have already been defined in our [constants.ts](https://github.com/Uniswap/examples/blob/main/v3-sdk/quoting/src/libs/constants.ts#L14) file: ```typescript const WETH_TOKEN = new Token( @@ -90,15 +114,10 @@ const USDC_TOKEN = new Token( 'USDC', 'USD//C' ) - -tokens: { - in: USDC_TOKEN, - amountIn: 1000, - out: WETH_TOKEN, - fee: FeeAmount.MEDIUM, -}, ``` +These constants are used in the `config.ts` file, as mentioned in the Introduction. + We can find the Pool Factory Contract address for our chain [here](../../../../contracts/v3/reference/Deployments.md). ## Referencing the Pool contract and fetching metadata @@ -138,6 +157,7 @@ const [token0, token1, fee, liquidity, slot0] = await Promise.all([ The return values of these methods will become inputs to the quote fetching function. The `token0` and `token1` variables are the addresses of the tokens in the Pool and should not be mistaken for `Token` objects from the sdk. +For the full code, check out [`getPoolConstants()`](https://github.com/Uniswap/examples/blob/main/v3-sdk/quoting/src/libs/quote.ts#L35) in `quote.ts`. :::note In this example, the metadata we fetch is already present in our inputs. This guide fetches this information first in order to show how to fetch any metadata, which will be expanded on in future guides. @@ -145,6 +165,9 @@ In this example, the metadata we fetch is already present in our inputs. This gu ## Referencing the Quoter contract and getting a quote +To get quotes for trades, Uniswap has deployed a **Quoter Contract**. We will use this contract to fetch the output amount we can expect for our trade, without actually executing the trade. +Check out the full code for the following snippets in [quote.ts](https://github.com/Uniswap/examples/blob/main/v3-sdk/quoting/src/libs/quote.ts) + Like we did for the Pool contract, we need to construct an instance of an **ethers** `Contract` for our Quoter contract in order to interact with it: ```typescript @@ -194,6 +217,13 @@ It should be noted that `quoteExactInputSingle` is only 1 of 4 different methods 3. `quoteExactOutputSingle` - given the amount you want to get out, produces a quote for the amount in for a swap over a single pool 4. `quoteExactOutput` - given the amount you want to get out, produces a quote for the amount in for a swap over multiple pools +If we want to trade two tokens that do not share a pool with each other, we will need to make swaps over multiple pools. +This is where the `quoteExactInput` and `quoteExactOutput` methods come in. +We will dive deeper into routing in the [routing guide](03-routing.md). + +For the `exactOutput` and `exactOutputSingle` methods, we need to keep in mind that a pool can not give us more than the amount of Tokens it holds. +If we try to get a quote on an output of 100 WETH from a Pool that only holds 50 WETH, the function call will fail. + ## Next Steps Now that you're able to make a quote, check out our next guide on [trading](./02-trading.md) using this quote! diff --git a/docs/sdk/v3/guides/swaps/02-trading.md b/docs/sdk/v3/guides/swaps/02-trading.md index 083830b2b8..868ed986db 100644 --- a/docs/sdk/v3/guides/swaps/02-trading.md +++ b/docs/sdk/v3/guides/swaps/02-trading.md @@ -9,6 +9,8 @@ This guide will build off our [quoting guide](./01-quoting.md) and show how to u :::info If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](../01-background.md) page! + +To get started with local development, also check out the [local development guide](../02-local-development.md). ::: In this example we will trade between two ERC20 tokens: **WETH and USDC**. The tokens, amount of input token, and the fee level can be configured as inputs. @@ -32,36 +34,112 @@ For this guide, the following Uniswap packages are used: The core code of this guide can be found in [`trading.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/src/libs/trading.ts) +## Using a wallet extension + +Like in the previous guide, our [example](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading) uses a [config file ](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/src/config.ts) to configurate the inputs used. +The strucuture is similar to the quoting config, but we also have the option to select an environment: + +```typescript +export interface ExampleConfig { + env: Environment + rpc: { + local: string + mainnet: string + } + wallet: { + address: string + privateKey: string + } + tokens: { + in: Token + amountIn: number + out: Token + poolFee: number + } +} +``` + +Per default, the env field is set to `Environment.LOCAL`: + +```typescript +export const CurrentConfig: ExampleConfig = { + env: Environment.LOCAL, + rpc: { + local: 'http://localhost:8545', + mainnet: '', + }, + wallet: { + address: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + privateKey: + '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', + }, + tokens: { + in: WETH_TOKEN, + amountIn: 1, + out: USDC_TOKEN, + poolFee: FeeAmount.MEDIUM, + }, +} +``` + +In this example, we have the option to use a Wallet Extension like Metamask to sign the transactions we are sending. To do so, let's change the Environment to `Environment.WALLET_EXTENSION`: + +```typescript +export const CurrentConfig: ExampleConfig = { + env: Environment.WALLET_EXTENSION, + rpc: { + local: 'http://localhost:8545', + }, + wallet: { + ... + }, + tokens: { + ... + }, +} +``` + +Run the example and then add the local network to your wallet browser extension, if you are using Metamask for example, follow [this guide](https://support.metamask.io/hc/en-us/articles/360043227612-How-to-add-a-custom-network-RPC). +You should also import a private key to use on your local network, for example `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80` from Foundry's example wallets. + +Consider checking out the [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/README.md) of the example. + +If you cannot see the Tokens traded in your wallet, you possibly have to [import them](https://support.metamask.io/hc/en-us/articles/360015489031-How-to-display-tokens-in-MetaMask). + ## Constructing a route from pool information To construct our trade, we will first create an model instance of a `Pool`. We create an **ethers** contract like in the [previous guide](./01-quoting.md#referencing-the-pool-contract-and-fetching-metadata). - We will first extract the needed metadata from the relevant pool contract. Metadata includes both constant information about the pool as well as information about its current state stored in its first slot: +We will first extract the needed metadata from the relevant pool contract. Metadata includes both constant information about the pool as well as information about its current state stored in its first slot: ```typescript async function getPoolInfo() { - const [token0, token1, fee, tickSpacing, liquidity, slot0] = + const [token0, token1, fee, liquidity, slot0] = await Promise.all([ - poolContract.token0(), - poolContract.token1(), poolContract.fee(), - poolContract.tickSpacing(), poolContract.liquidity(), poolContract.slot0(), ]) return { - token0, - token1, fee, - tickSpacing, liquidity, sqrtPriceX96: slot0[0], tick: slot0[1], } } - ``` +Before continuing, let's talk about the values we fetched here and what they represent: + +- `fee` is the fee that is taken from every swap that is executed on the pool in 1 per million - if the `fee` value of a pool is 500, ```500/ 1000000``` (or 0.05%) of the trade amount is taken as a fee. This fee goes to the liquidity providers of the Pool. +- `liquidity` is the amount of liquidity the Pool can use for trades at the current price. +- `sqrtPriceX96` is the current Price of the pool, encoded as a ratio between `token0` and `token1`. +- `tick` is the tick at the current price of the pool. + +Check out the [whitepaper](https://uniswap.org/whitepaper-v3.pdf) to learn more on how liquidity and ticks work in Uniswap V3. + +You can find the full code in [`pool.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/src/libs/pool.ts). + Using this metadata along with our inputs, we will then construct a `Pool`: ```typescript @@ -77,7 +155,25 @@ const pool = new Pool( ) ``` -With this `Pool`, we can now construct a route to use in our trade. We will reuse our previous quoting code to calculate the output amount we expect from our trade: +## Creating a Route + +With this `Pool`, we can now construct a route to use in our trade. Routes represent a route over one or more pools from one Token to another. Let's imagine we have three pools: + +``` +- PoolA: USDC/ WETH +- PoolB: USDT/ WETH +- PoolC: USDT/ DAI +``` + +We would like to trade from USDC to DAI, so we create a route through our 3 pools: + +``` +PoolA -> PoolB -> PoolC +``` + +The `Route` object can find this route from an array of given pools and an input and output Token. + +To keep it simple for this guide, we only swap over one Pool: ```typescript import { Route } from '@uniswap/v3-sdk' @@ -89,6 +185,9 @@ const swapRoute = new Route( ) ``` +Our `Route` understands that `CurrentConfig.tokens.in` should be traded for `CurrentConfig.tokens.out` over the Array of pools `[pool]`. + + ## Constructing an unchecked trade Once we have constructed the route object, we now need to obtain a quote for the given `inputAmount` of the example: @@ -130,7 +229,8 @@ const quoteCallReturnData = await provider.call({ return ethers.utils.defaultAbiCoder.decode(['uint256'], quoteCallReturnData) ``` -With the quote and the route, we can now construct an unchecked trade using the route in addition to the output amount from a quote based on our input: +With the quote and the route, we can now construct a trade using the route in addition to the output amount from a quote based on our input. +Because we already know the expected output of our Trade, we do not have to check it again. We can use the `uncheckedTrade` function to create our Trade: ```typescript import { Trade } from 'uniswap/v3-sdk' @@ -180,7 +280,11 @@ const options: SwapOptions = { } ``` -Next, we use the Uniswap `SwapRouter` to get the associated call parameters for our trade and options: +The slippage of our trade is the maximum decrease from our calculated output amount that we are willing to accept for this trade. +The deadline is the latest point in time when we want the transaction to go through. +If we set this value too high, the transaction could be left waiting for days and we would need to pay gas fees to cancel it. + +Next, we use the `SwapRouter` class, a representation of the Uniswap [SwapRouter Contract](https://github.com/Uniswap/v3-periphery/blob/v1.0.0/contracts/SwapRouter.sol), to get the associated call parameters for our trade and options: ```typescript import { SwapRouter } from '@uniswap/v3-sdk' diff --git a/docs/sdk/v3/guides/swaps/03-routing.md b/docs/sdk/v3/guides/swaps/03-routing.md index 1b823d1eb6..2e35b64590 100644 --- a/docs/sdk/v3/guides/swaps/03-routing.md +++ b/docs/sdk/v3/guides/swaps/03-routing.md @@ -71,7 +71,8 @@ const router = new AlphaRouter({ ## Creating a route -Next, we will create our options conforming to the `SwapOptionsSwapRouter02` interface, defining the wallet to use, slippage tolerance, and deadline for the transaction: +We will use the [SwapRouter02](https://github.com/Uniswap/v3-periphery/blob/v1.0.0/contracts/SwapRouter.sol) for our trade. +The `smart-order-router` package provides us with a SwapOptionsSwapRouter02` interface, defining the wallet to use, slippage tolerance, and deadline for the transaction that we need to interact with the contract: ```typescript import { SwapOptionsSwapRouter02, SwapType } from '@uniswap/smart-order-router' @@ -85,6 +86,8 @@ const options: SwapOptionsSwapRouter02 = { } ``` +Like explained in the [previous guide](./02-trading.md#executing-a-trade), it is important to set the parameters to sensible values. + Using these options, we can now create a trade (`TradeType.EXACT_INPUT` or `TradeType.EXACT_OUTPUT`) with the currency and the input amount to use to get a quote. For this example, we'll use an `EXACT_INPUT` trade to get a quote outputted in the quote currency. ```typescript @@ -106,7 +109,21 @@ const route = await router.route( ) ``` -The `fromReadableAmount` function calculates the amount of tokens in the Token's smallest unit from the full unit and the Token's decimals. +The `fromReadableAmount` function calculates the amount of tokens in the Token's smallest unit from the full unit and the Token's decimals: + +```typescript title="src/libs/conversion.ts" +export function fromReadableAmount(amount: number, decimals: number): JSBI { + const extraDigits = Math.pow(10, countDecimals(amount)) + const adjustedAmount = amount * extraDigits + return JSBI.divide( + JSBI.multiply( + JSBI.BigInt(adjustedAmount), + JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(decimals)) + ), + JSBI.BigInt(extraDigits) + ) +} +``` `route` and `route.methodParameters` are *optional* as the request can fail, for example if **no route exists** between the two Tokens or because of networking issues. We check if the call was succesful: @@ -139,7 +156,14 @@ const tokenApproval = await tokenContract.approve( ) ``` -We can get the **V3_SWAP_ROUTER_ADDRESS** for our chain from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). We need to wait one block for the approval transaction to be included by the blockchain. +To be able to spend the tokens of a wallet, a smart contract first needs to get an approval from that wallet. +ERC20 tokens have an `approve` function that accepts the address of the smart contract that we want to allow spending our tokens and the amount the smart contract should be allowed to spend. + +We can get the **V3_SWAP_ROUTER_ADDRESS** for our chain from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). +Keep in mind that different chains might have **different deployment addresses** for the same contracts. +The deployment address for local forks of a network are the same as in the network you forked, so for a **fork of mainnet** it would be the address for **Mainnet**. + +We need to wait one block for the approval transaction to be included by the blockchain. Once the approval has been granted, we can now execute the trade using the route's computed calldata, values, and gas values: @@ -156,6 +180,8 @@ const txRes = await wallet.sendTransaction({ After swapping, you should see the currency balances update in the UI shortly after the block is confirmed. +You can find the full code in [`routing.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/routing/src/libs/routing.ts). + ## Next Steps Now that you're familiar with trading, consider checking out our next guides on [pooling liquidity](../liquidity/01-position-data.md) to Uniswap! From 237b6d5417c6f3962050f8186bb9300bb59b5112 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Tue, 8 Aug 2023 11:59:22 +0100 Subject: [PATCH 27/52] Rename sdk guides advanced/overview to introduction, update pool-data guide --- .../{01-overview.md => 01-introduction.md} | 4 +- docs/sdk/v3/guides/advanced/02-pool-data.md | 133 +++++++++++++++--- .../guides/advanced/images/tickBitmap_cut.png | Bin 0 -> 123856 bytes 3 files changed, 116 insertions(+), 21 deletions(-) rename docs/sdk/v3/guides/advanced/{01-overview.md => 01-introduction.md} (96%) create mode 100644 docs/sdk/v3/guides/advanced/images/tickBitmap_cut.png diff --git a/docs/sdk/v3/guides/advanced/01-overview.md b/docs/sdk/v3/guides/advanced/01-introduction.md similarity index 96% rename from docs/sdk/v3/guides/advanced/01-overview.md rename to docs/sdk/v3/guides/advanced/01-introduction.md index 477304f783..07fc66395f 100644 --- a/docs/sdk/v3/guides/advanced/01-overview.md +++ b/docs/sdk/v3/guides/advanced/01-introduction.md @@ -1,6 +1,6 @@ --- -id: overview -title: Overview +id: introduction +title: Introduction --- For some more advanced use cases, it is necessary to use multiple tools in the Uniswap toolchain. diff --git a/docs/sdk/v3/guides/advanced/02-pool-data.md b/docs/sdk/v3/guides/advanced/02-pool-data.md index 8f2f979228..4e08a12124 100644 --- a/docs/sdk/v3/guides/advanced/02-pool-data.md +++ b/docs/sdk/v3/guides/advanced/02-pool-data.md @@ -34,24 +34,64 @@ We will also use the `ethers-multicall` npm package: The core code of this guide can be found in [`fetcher.ts`](https://github.com/Uniswap/examples/tree/main/v3-sdk/multicall/src/libs/fetcher.ts) +## Configuration + +The example accompanying this guide can be configured in the [`config.ts`](https://github.com/Uniswap/examples/tree/main/v3-sdk/multicall/src/config.ts) file. +The default configuration defines the rpc endpoint and the pool that is used for this guide: + +```typescript +export const CurrentConfig: ExampleConfig = { + env: Environment.MAINNET, + rpc: { + local: 'http://localhost:8545', + mainnet: 'https://mainnet.infura.io/v3/0ac57a06f2994538829c14745750d721', + }, + ... + pool: { + token0: USDC_TOKEN, + token1: WETH_TOKEN, + fee: FeeAmount.MEDIUM, + }, +} +``` + +FeeAmount.MEDIUM means that the pool has a swap fee of **0.3%**. +The `USDC_TOKEN` and `WETH_TOKEN` are defined in the [`constants.ts`](https://github.com/Uniswap/examples/tree/main/v3-sdk/multicall/src/libs/constants.ts) file: + +```typescript +export const WETH_TOKEN = new Token( + SupportedChainId.MAINNET, + '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + 18, + 'WETH', + 'Wrapped Ether' +) + +export const USDC_TOKEN = new Token( + SupportedChainId.MAINNET, + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + 6, + 'USDC', + 'USD//C' +) +``` + ## Computing the Pool's deployment address -In this example, we will construct the **USDC - WETH** Pool with **LOW** fees. The SDK provides a method to compute the address: +In this example, we will construct the **USDC - WETH** Pool with **MEDIUM** fees. The SDK provides a method to compute the address: ```typescript -import { Pool, FeeAmount } from '@uniswap/v3-sdk' -import { Token, WETH9 } from '@uniswap/sdk-core' +import { Pool } from '@uniswap/v3-sdk' +import { CurrentConfig } from '../config.ts' -const USDC = new Token(1, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 6) -const WETH = WETH9[USDC.chainId] const poolAddress = Pool.getAddress( - USDC, - WETH, - FeeAmount.LOW + CurrentConfig.pool.token0, + CurrentConfig.pool.token1, + CurrentConfig.pool.fee ) ``` -Uniswap V3 allows different Fee tiers when deploying a pool, so multiple pools can exist for each pair of tokens. FeeAmount.LOW means the pool has a swap fee of 0.05%. +Uniswap V3 allows 4 different Fee tiers when deploying a pool, so multiple pools can exist for each pair of tokens. ## Creating a Pool Contract instance and fetching metadata @@ -62,7 +102,7 @@ To construct the Contract we need to provide the address of the contract, its AB import { ethers } from 'ethers import IUniswapV3PoolABI from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json' -const provider = new ethers.provider.JsonRpcProvider(rpcUrl) +const provider = getProvider() const poolContract = new ethers.Contract( poolAddress, IUniswapV3PoolABI.abi, @@ -70,6 +110,8 @@ const poolContract = new ethers.Contract( ) ``` +The `getProvider()` function returns an `ethers.providers.JsonRpcProvider` with either the local or mainnet rpc url that we defined, depending on the Environment that we set in `config.ts`. + Once we have set up our reference to the contract, we can proceed to access its methods. To construct our offchain representation of the Pool Contract, we need to fetch its liquidity, sqrtPrice, currently active tick and the full Tick data. We get the **liquidity**, **sqrtPrice** and **tick** directly from the blockchain by calling `liquidity()`and `slot0()` on the Pool contract: @@ -104,7 +146,49 @@ V3 pools use ticks to [concentrate liquidity](../../../../concepts/protocol/conc Even though most Pools only have a couple of **initialized ticks**, it is possible that a pools liquidity is defined by thousands of **initialized ticks**. In that case, it can be very expensive or slow to get all of them with normal RPC calls. -### Multicall +If you are not familiar with the concept of ticks, check out the [`introduction`](./01-introduction.md). + +To access tick data, we will use the `ticks` function of the V3 Pool contract: + +```solidity + function ticks( + int24 tick + ) external view returns ( + uint128 liquidityGross, + int128 liquidityNet, + uint256 feeGrowthOutside0X128, + uint256 feeGrowthOutside1X128, + int56 tickCumulativeOutside, + uint160 secondsPerLiquidityOutsideX128, + uint32 secondsOutside, + bool initialized + ) +``` + +The `tick` parameter that we provide the function with is the **index** (memory position) of the Tick we are trying to fetch. +To get the indices of all initialized Ticks of the Pool, we can calculate them from the **tickBitmaps**. +To fetch a `tickBitmap` function of the V3 Pool: + +```solidity + function tickBitmap( + int16 wordPosition + ) external view returns (uint256) +``` + +A pool stores lots of bitmaps, each of which contain the status of 256 Ticks. +The parameter `int16 wordPosition` the function accepts is the position of the bitMap we want to fetch. +We can calculate all the position of bitMaps (or words as they are sometimes called) from the `tickSpacing` of the Pool, which is in turn dependant on the Fee tier. + +So to summarise we need 4 steps to fetch all initialized ticks: + +1. Calculate all bitMap positions from the tickSpacing of the Pool. +2. Fetch all bitMaps using their positions. +3. Calculate the memory positions of all Ticks from the bitMaps. +4. Fetch all Ticks by their memory position. + +We will use multicalls for the fetch calls. + +## Multicall Multicall contracts **aggregate results** from multiple contract calls and therefore allow sending multiple contract calls in **one RPC request**. This can improve the **speed** of fetching large amounts of data significantly and ensures that the data fetched is all from the **same block**. @@ -112,9 +196,9 @@ This can improve the **speed** of fetching large amounts of data significantly a We will use the Multicall2 contract by MakerDAO. We use the `ethers-muticall` npm package to easily interact with the Contract. -### Fetching the positions of initialized Ticks +## Calculating all bitMap positions -Uniswap V3 Pools store **bitmaps**, also called *words*, that represent the state of **256 initializable ticks** at a time. +As mentioned, Uniswap V3 Pools store **bitmaps**, also called *words*, that represent the state of **256 initializable ticks** at a time. The value at a bit of a word is 1 if the tick at this index is initialized and 0 if it isn't. We can calculate the positions of initialized ticks from the **words** of the Pool. @@ -137,7 +221,9 @@ const maxWord = tickToWord(887272) Ticks can only be initialized at indices that are **divisible by the tickSpacing**. One word contains 256 ticks, so we can compress the ticks by right shifting 8 bit. -Knowing the word indices, we can now fetch them from the Pool using multicall and the `tickBitmap` read call. +## Fetching bitMaps from their position + +Knowing the positions of words in the Pool contract, we can now fetch them from the Pool using multicall and the `tickBitmap` read call. First we initialize our multicall providers and Pool Contract: @@ -154,7 +240,7 @@ const poolContract = new Contract(poolAddress, IUniswapV3PoolABI.abi) The `multicallProvider` creates the multicall request and sends it via the ethers Provider. -Next we loop through all possible word indices and add a `tickBitmap` call for each: +Next we loop through all possible word positions and add a `tickBitmap` call for each: ```typescript let calls: any[] = [] @@ -165,7 +251,7 @@ for (let i = minWord; i <= maxWord; i++) { } ``` -We also keep track of the word indices to be able to loop through them in the same order we added the calls to the array. +We also keep track of the word position indices to be able to loop through them in the same order we added the calls to the array. We use the `multicallProvider.all()` function to send a multicall and map the results: @@ -177,14 +263,23 @@ const results: bigint[] = (await multicallProvider.all(calls)).map( ) ``` -Lastly, we check which ticks are initialized in the words we fetched and calculate the **tick position** from the **word index** and the **tickSpacing** of the pool. +A great visualization of what the bitMaps look like can be found in the [Uniswap V3 development book](https://uniswapv3book.com/docs/milestone_2/tick-bitmap-index/): + +TickBitmap -We check if a tick is **initialized** inside the word by shifting it to the position and performing a bitwise AND operation: +We encourage anyone trying to get a deeper understanding of the Uniswap protocol to read the Uniswap V3 Book. + +## Calculating the memory positions of all Ticks + +Now that we fetched all **bitMaps**, we check which ticks are initialized and calculate the **tick position** from the **word index** and the **tickSpacing** of the pool. + +We check if a tick is **initialized** inside the word by shifting a bit by the index we are looking at and performing a bitwise AND operation: ```typescript const bit = 1n const initialized = (bitmap & (bit << BigInt(i))) !== 0n ``` + If the tick is **initialized**, we revert the compression from tick to word we made earlier by multiplying the word index with 256, which is the same as left shifting by 8 bit, adding the position we are currently at, and multiplying with the tickSpacing: ```typescript @@ -215,7 +310,7 @@ const tickIndices: number[] = [] We now have an array containing the indices of all initialized Ticks. -### Fetching all Ticks by their indices +## Fetching all Ticks by their indices We use the multicallProvider again to execute an aggregated read call for all tick indices. We create an array of call Promises again and use `.all()` to make our multicall: diff --git a/docs/sdk/v3/guides/advanced/images/tickBitmap_cut.png b/docs/sdk/v3/guides/advanced/images/tickBitmap_cut.png new file mode 100644 index 0000000000000000000000000000000000000000..1729004e4d51324ef3aa1215b09f2e60d55725e3 GIT binary patch literal 123856 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

Uobqov?uknZ8Yf1D%xXaU; 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 P1|aZs^>bP0l+XkKJRFWY literal 0 HcmV?d00001 From 89a5de711f7ab2eaa657f56bb1f598553ff56055 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Wed, 9 Aug 2023 11:38:32 +0100 Subject: [PATCH 28/52] Add theoretical background to introduction --- .../sdk/v3/guides/advanced/01-introduction.md | 88 ++++++++++++++++++ .../advanced/images/tickBitmap-etherscan.png | Bin 0 -> 22436 bytes 2 files changed, 88 insertions(+) create mode 100644 docs/sdk/v3/guides/advanced/images/tickBitmap-etherscan.png diff --git a/docs/sdk/v3/guides/advanced/01-introduction.md b/docs/sdk/v3/guides/advanced/01-introduction.md index 07fc66395f..ba836dbcb4 100644 --- a/docs/sdk/v3/guides/advanced/01-introduction.md +++ b/docs/sdk/v3/guides/advanced/01-introduction.md @@ -15,3 +15,91 @@ We will take a deep dive into the Uniswap V3 protocol and use practical examples We will explore how we can compute the available liquidity in a specific price range, visualize **liquidity density** in pools, use Uniswap as a **price oracle** and swap by creating **Range Orders**. These guides are a bit longer than the previous ones and provide more theoretical background. + +## Theoretical background + +Some of the guides presented here require a bit of theoretical and mathematical background. +To get the most out of the advanced guides, we encourage you to take a step back and read a bit about the math and theories behind the Uniswap protocol. + +The most complete source of information on the Uniswap protocol is the [Uniswap V3 book](https://uniswapv3book.com/). + +Besides the [concepts](../../../../concepts/uniswap-protocol.md) section of the Docs, the [Uniswap V3 whitepaper](https://uniswap.org/whitepaper-v3.pdf) is a great introduction to the protocol. +If you haven't checked it out yet, it is probably more concise and easier to understand than you would expect. + +### Datatypes in Solidity + +Uniswap V3 pools make use of a number of Datatypes Solidity offers to efficiently store their state. +If you are not familiar with Solidity data types yet, it can help to take a look at the [Solidity language reference](https://docs.soliditylang.org/en/v0.8.7/types.html#). +For the following guides, it is beneficial to take a look at two of them, which we will outline here. + +Ticks are stored as a [mapping(int24 => Tick.Info)](https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L93). +Solidity [mappings](https://docs.soliditylang.org/en/v0.8.7/types.html#mapping-types) are very similar to hash maps, such that we can access any Value with their key with just one read operation. +The [`Tick.Info`](https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/Tick.sol#L17) stores the values of the Tick that we need to work with the Pool: + +```solidity +struct Info { + // the total position liquidity that references this tick + uint128 liquidityGross; + // amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left), + int128 liquidityNet; + // fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) + // only has relative meaning, not absolute — the value depends on when the tick is initialized + uint256 feeGrowthOutside0X128; + uint256 feeGrowthOutside1X128; + // the cumulative tick value on the other side of the tick + int56 tickCumulativeOutside; + // the seconds per unit of liquidity on the _other_ side of this tick (relative to the current tick) + // only has relative meaning, not absolute — the value depends on when the tick is initialized + uint160 secondsPerLiquidityOutsideX128; + // the seconds spent on the other side of the tick (relative to the current tick) + // only has relative meaning, not absolute — the value depends on when the tick is initialized + uint32 secondsOutside; + // true iff the tick is initialized, i.e. the value is exactly equivalent to the expression liquidityGross != 0 + // these 8 bits are set to prevent fresh sstores when crossing newly initialized ticks + bool initialized; + } +``` + +We will use most of these values in the following guides. + +In our case, we can access any `Tick.Info` value stored in the pool by its `int24` key. +The key of the Tick is usually called its *index*. +Mappings are not iterable, so if we are trying to fetch all the Ticks stored in a Pool, we can't just iterate over the mapping. +Instead, we have to know the keys (indices) of the mapping, we will explore how to do that in the [Pool data guide](./02-pool-data.md). + +The second Solidity datatype we need to understand are normal unsigned [Integers](https://docs.soliditylang.org/en/v0.8.7/types.html#integers). +Solidity supports unsigned integer sizes between `uint8` and `uint256`, which are 8 and 256 bits long respectively. + +Let's take a look at the `tickBitmap` function of a V3 Pool: + +```solidity + function tickBitmap( + int16 wordPosition + ) external view returns (uint256) +``` + +Similar to the tick mapping, the tickBitmap is a mapping of type [mapping(int16 => uint256)](https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L95). + +Let's look at the WETH/USDC pool with LOW fee on [Etherscan](https://etherscan.io/address/0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#readContract). +If we call the `tickBitmap` function with the input `0` we get the following response (at the time of writing): + +TickBitmapEtherscan + +The `uint256` return value is interpreted as the representation of a decimal number by Etherscan. +The actual raw return value are 256 bits, that look something like this: + +```raw +0x0000 ... lots of zeros and ones ... 000000110000000000 +``` + +We interpret this string of zeros and ones not as the representation of a number, but rather as 256 booleans. + +If the value of a position in the value is 1, the Tick at this position is **initialized**, meaning it holds a value. + +With this trick, V3 Pools allow us to fetch the status of **256 ticks** with one call. +We will go into more details on how to calculate the tick indices from the tickBitmaps we fetch in the following guides. + +## History of Uniswap + +To get a better understanding of the V3 protocol, it can also be beneficial to understand the **history of decentralized exchanges** and the Uniswap protocol since it was founded in 2018. +You can read more about the older versions of Uniswap in the [V1](https://hackmd.io/@HaydenAdams/HJ9jLsfTz?type=view#Swaps-vs-Transfers) and [V2](https://uniswap.org/whitepaper.pdf) whitepapers, as well as the [V1](../../../../contracts/v1/overview.md) and [V2](../../../../contracts/v2/overview.md) section in the contract section. diff --git a/docs/sdk/v3/guides/advanced/images/tickBitmap-etherscan.png b/docs/sdk/v3/guides/advanced/images/tickBitmap-etherscan.png new file mode 100644 index 0000000000000000000000000000000000000000..208d2db12942c98de041f09971981a064134d440 GIT binary patch literal 22436 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 VNS4T!bABGHnzHue@<-2J{yzb&t7!lL literal 0 HcmV?d00001 From d00893c84ed76ffde1544b4036234b089417ec62 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Wed, 9 Aug 2023 12:11:16 +0100 Subject: [PATCH 29/52] Update active liquidity guide with feedback --- .../v3/guides/advanced/03-active-liquidity.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/sdk/v3/guides/advanced/03-active-liquidity.md b/docs/sdk/v3/guides/advanced/03-active-liquidity.md index df2470ee6e..4528dfeffb 100644 --- a/docs/sdk/v3/guides/advanced/03-active-liquidity.md +++ b/docs/sdk/v3/guides/advanced/03-active-liquidity.md @@ -43,13 +43,17 @@ This is what the **Initialized Ticks** of a Pool represent - they are a represen LiquidityNet1 When entering or leaving a position, its liquidity is added or removed from the **active liquidity available** for a Swap. -The initialized Ticks store this change in available liquidity in the `liquidityNet` field. +The initialized Ticks store this **change in available liquidity** in the `liquidityNet` field. The change is always stored in relation to the currently active Tick - the current price. When the price crosses an initialized Tick, it gets updated and liqudity that was previously added when crossing the Tick would now be removed and vice versa. -### Fetching initialized Ticks +The `liquidityGross` value represents the gross value of liquidity referencing the tick. +This is important for the edge case that one position ends at a Tick and a second position with exactly the same liquidity value would start at the Tick. +In this case `liquidityNet` would be **0** but `liquidityGross` would still have a value, which ensures that the Tick is not deleted from the Pool. + +To visualize liquidity in a graph, we will only need to consider the changes, so it's sufficient to fetch the Ticks with `liquidityNet` not 0. -To demonstrate another way +### Fetching initialized Ticks To fetch all ticks of our Pool, we will use the [Uniswap V3 graph](../../../../api/subgraph/overview.md). To visualize active liquidity, we need the **tickIdx**, the **liquidityGross** and the **liquidityNet**. @@ -104,7 +108,8 @@ The active liqudity at the current Price is also stored in the smart contract - ### Tickspacing -Only the Ticks with indices that are dividable with 0 remainder by the tickspacing of a Pool are initializable. +Only the Ticks with indices that are divisible with 0 remainder by the tickspacing of a Pool are initializable. +This is a convention defined by the protocol to save gas. The Tickspacing of the Pool is dependent on the Fee Tier. Pools with lower fees are meant to be used for more stable Token Pairs and allow for more granularity in where LPs position their liquidity. @@ -129,6 +134,8 @@ Instead, we will display a sensible number of Ticks around the current price. We know the spacing between Ticks and the Initialized Ticks where active liquidity changes. All we have to do is start calculating from the current Tick and iterate outwards. +The code mentioned in the following snippets can be found in [`active-liquidity.ts`](https://github.com/Uniswap/examples/tree/main/v3-sdk/pool-data/src/libs/active-liquidity.ts). + To draw our chart we want a data structure that looks something like this: ```typescript @@ -150,7 +157,7 @@ const tickIdxToTickDictionary: Record = Object.fromEntries( ) ``` -The Ticks variable in this code snippet is the response we got from the V3 Subgraph. +The `ticks` variable in this code snippet is the result we got from the V3 Subgraph earlier. We want to mark the Tick closest to the current Price and we want to be able to display the prices at a Tick to the user. We calculate the **initializable Tick** closest to the current price and create the active Tick that we start from: From a6204f62d24148bd781d04382d6343939c14b4fb Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Wed, 9 Aug 2023 14:12:58 +0100 Subject: [PATCH 30/52] Update price oracle and range orders guide --- .../sdk/v3/guides/advanced/04-price-oracle.md | 15 ++++++++++- .../sdk/v3/guides/advanced/05-range-orders.md | 27 ++++++++++++++++--- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/docs/sdk/v3/guides/advanced/04-price-oracle.md b/docs/sdk/v3/guides/advanced/04-price-oracle.md index 3c73bc056d..9b9bc9e7e4 100644 --- a/docs/sdk/v3/guides/advanced/04-price-oracle.md +++ b/docs/sdk/v3/guides/advanced/04-price-oracle.md @@ -59,7 +59,17 @@ interface Observation { } ``` -To fetch the `Observations` from our pool contract, we will use the [`observe`](../../../../contracts/v3/reference/core/UniswapV3Pool.md#observe) function. +To fetch the `Observations` from our pool contract, we will use the [`observe`](../../../../contracts/v3/reference/core/UniswapV3Pool.md#observe) function: + +```solidity +function observe( + uint32[] secondsAgos +) external view override noDelegateCall returns ( + int56[] tickCumulatives, + uint160[] secondsPerLiquidityCumulativeX128s +) +``` + We first check how many observations are stored in the Pool by calling the `slot0` function. ```typescript @@ -220,6 +230,8 @@ struct Observation { It is possible to request any Observation up to (excluding) index `65535`, but indices equal to or greater than the `observationCardinality` will return uninitialized Observations. +The full code to the following code snippets can be found in [`oracle.ts`](https://github.com/uniswap/examples/blob/main/v3-sdk/oracle/src/libs/oracle.ts) + ```typescript let requests = [] for (let i = 0; i < 10; i++) { @@ -241,6 +253,7 @@ Because we access indices of an array, this would give us an unexpected result t ::: One way to handle this behaviour is deploying or [using](https://github.com/mds1/multicall) a Contract with a [multicall](https://solidity-by-example.org/app/multi-call/) functionality to get all observations with one request. +You can also find an example of a JS multicall in the [Pool data guide](./02-pool-data.md). We map the RPC result to the Typescript interface that we created: diff --git a/docs/sdk/v3/guides/advanced/05-range-orders.md b/docs/sdk/v3/guides/advanced/05-range-orders.md index daa24f8be2..a1f7f30376 100644 --- a/docs/sdk/v3/guides/advanced/05-range-orders.md +++ b/docs/sdk/v3/guides/advanced/05-range-orders.md @@ -128,7 +128,7 @@ let targetTick = nearestUsableTick( ) ``` -This nearest Tick will most likely not exactly match our Price target. +This nearest Tick will most likely not **exactly** match our Price target. Depending on our personal preferences we can either err on the higher or lower side of our target by adding or subtracting the `tickSpacing` if the initializable Tick is lower or higher than the theoretically closest Tick. @@ -157,6 +157,8 @@ We now have a lower and upper Tick for our Position, next we need to construct a We will use the `NonfungiblePositionManager` and `Position` classes from the `v3-sdk` to construct our position. We then use an **etherJS** wallet to mint our Position on-chain. +If you are not familiar with liquidity Positions, check out the [liquidity position guides](../liquidity/01-position-data.md). + ### Minting the Position We create a `Position` object with our ticks and the amount of tokens we want to deposit: @@ -175,6 +177,8 @@ const position = Position.fromAmount0({ Before we mint our position, we need to give the `NonfungiblePositionManager` Contract an approval to transfer our tokens. We can find the Contract address on the official [Uniswap Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). +For local development, the contract address is the same as the network we are forking from. +So if we are using a local fork of mainnet like described in the [Local development guide](../02-local-development.md), the contract address would be the same as on mainnet. ```typescript import ethers from 'ethers' @@ -227,9 +231,12 @@ We can populate our mint transaction and send it with our wallet: const txRes = await wallet.sendTransaction(transaction) ``` +You can find full code examples for these code snippets in [`range-order.ts`](https://github.com/uniswap/examples/blob/main/v3-sdk/range-order/src/libs/range-order.ts). + ### Getting the tokenId -We want to read the response to our `Mint` function call to get the position id. +We want to read the response to our `Mint` function call to get **the position id**. +We will need the positionId to fetch the Position Info from the NFTPositionManager contract. We wait for the transaction receipt and fetch the result using `trace_transaction`: ```typescript @@ -254,7 +261,13 @@ while (receipt === null) { } ``` -Your Node provider may not support this call. In that case you can also call the NonfungiblePositionManager Contract with the wallet address and identify the Range Order Position manually. +Your Node provider may not support this call. In that case you can also call the NonfungiblePositionManager Contract with the wallet address and identify the Range Order Position manually: + +```typescript +const mintCallOutput = await wallet.call(transaction) +``` + +We get a raw byte string as a return value from this function and have to parse it ourselves. We decode the result with the **ethers AbiCoder**. The solidity function has this signature: ```solidity @@ -274,9 +287,15 @@ const decodedOutput = ethers.utils.defaultAbiCoder.decode( const tokenId = decodedOutput.toString() ``` +Ethers handles the string decoding of the byte string we got and parses it to its internal datatypes. +The decodedOutput we get from the AbiCoder is a `ethers.Bignumber` so we need to cast it to a string to use it with the SDK. + We have created our Range Order Position, now we need to monitor it. -The decodedOutput we get from the AbiCoder is a `Bignumber` so we need to cast it to a string to use it with the SDK. +In the [code example](https://github.com/uniswap/examples/blob/main/v3-sdk/range-order/src/libs/range-order.ts#L180) we use `wallet.call` to get the position id. +`call` and `trace_call` both simulate a transaction on the connected node and return the expected output, `trace_call` gives us a much more detailed output though. +Depending on the use case, either can be the better choice. +In a production environment you would prefer to wait for the `transactionReceipt` like described earlier to ensure the transaction was actaully included in the blockchain. ## Observing the Price From c9bb3da048e6abf9788cf39a9921f41281308d25 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Fri, 11 Aug 2023 17:39:38 +0100 Subject: [PATCH 31/52] Update liquidity guides based on foundation feedback. --- .../v3/guides/liquidity/01-position-data.md | 28 +++++++++++++- .../guides/liquidity/02-minting-position.md | 19 +++++++++- .../guides/liquidity/03-fetching-positions.md | 28 ++++++++++---- .../guides/liquidity/04-modifying-position.md | 37 +++++++++++++++++++ .../v3/guides/liquidity/05-collecting-fees.md | 23 +++++++++++- 5 files changed, 124 insertions(+), 11 deletions(-) diff --git a/docs/sdk/v3/guides/liquidity/01-position-data.md b/docs/sdk/v3/guides/liquidity/01-position-data.md index 86adc92f93..0aa4bafd21 100644 --- a/docs/sdk/v3/guides/liquidity/01-position-data.md +++ b/docs/sdk/v3/guides/liquidity/01-position-data.md @@ -25,14 +25,40 @@ The code mentioned in this guide can be found across the [minting Position](http To understand what Positions are, we need to understand some underlying concepts of the Uniswap protocol. +Consider checking out the [Concepts section](../../../../concepts/protocol/concentrated-liquidity.md) as well as the [Uniswap Book](https://uniswapv3book.com/docs/introduction/uniswap-v3/). + ### Concentrated liquidity -Uniswap V3 Pools +Uniswap V3 Pools use concentrated liquidity to allow a denser concentration of liquidity at specific prices. +Compared to the full range liquidity model Uniswap V2 uses, this allows traders to make larger trades with less price impact. +Liquidity providers can choose a specific price range in which they want their liquidity to be used by trades. + +To achieve this, Uniswap V3 Pools discriminate the price range with **Ticks**. ### Ticks +Ticks are the boundaries between discrete price ranges. +A change of 1 Tick always represents a price change of 0.01% from the current price. +Uniswap V3 Pools can have different `tickSpacings`, a constant that describes which ticks can be used by the Pool. +Only ticks at indices that are divisible by the tickSpacing can be initialized. +This value is dependant on the fee of the Pool, Pools with higher fees have higher tickSpacing. + +For example, a Pool with **HIGH** fee (1%) has a tickSpacing of 200, meaning the price difference between initializable Ticks is: + +$$1.0001^{200} = 1.0202$$ or $$2.02$$% + ### Liquidity Positions +When someone provides liquidity to a Pool, they create a **Liquidity Position**. +This position is defined by the amount of liquidity provided and the start tick and the end tick, or price range, of the Position. + +Because V3 Pools allow users to choose any price range in which they want to provide liquidity, it is possible to create positions that do not contain the current Price of the Pool. +In this case, the liquidity provider will pay only one type of Token into the Pool, creating a **single side liquidity position**. + +To learn more about how Ticks and Liquidity positions work, consider reading the [whitepaper](https://uniswap.org/whitepaper-v3.pdf) or the other resources mentioned above. + +Now that we have a rough understanding of liquidity positions in Uniswap V3, let's look at the correspondent classes the SDK offers us. + ## Position class The **sdk** provides a [`Position`](https://github.com/Uniswap/v3-sdk/blob/main/src/entities/position.ts) class used to create local representations of an onchain position. diff --git a/docs/sdk/v3/guides/liquidity/02-minting-position.md b/docs/sdk/v3/guides/liquidity/02-minting-position.md index 2cac516da5..e14e7d1ecf 100644 --- a/docs/sdk/v3/guides/liquidity/02-minting-position.md +++ b/docs/sdk/v3/guides/liquidity/02-minting-position.md @@ -34,7 +34,11 @@ The core code of this guide can be found in [`mintPosition()`](https://github.co ## Giving approval to transfer our tokens -The first step is to give approval to the protocol's `NonfungiblePositionManager` to transfer our tokens: +We want to use the `NonfungiblePositionManager` contract to create our liqudity position. +In situations where a smart contract is transfering tokens on our behalf, we need to give it approval to do so. +This is done by interacting with the Contract of the contract, considering ERC20 Tokens are smart contracts of their own. + +Considering this, the first step to create our position is to give approval to the protocol's `NonfungiblePositionManager` to transfer our tokens: ```typescript const token0Approval = await getTokenTransferApproval( @@ -133,6 +137,11 @@ const configuredPool = new Pool( ) ``` +We need a Pool instance to create our Position as various parameters of liquidity positions depend on the state of the Pool where they are created. +An example is the current price (named *sqrtPriceX96* after the way it is encoded) to know the ratio of the two Tokens we need to send to the Pool. + +Liquidity provided below the current Price will be provided in the first Token of the Pool, while liquidity provided above the current Price is made up by the second Token. + ## Calculating our `Position` from our input tokens Having created the instance of the `Pool` class, we can now use that to create an instance of a `Position` class, which represents the price range for a specific pool that LPs choose to provide in: @@ -187,7 +196,13 @@ const { calldata, value } = NonfungiblePositionManager.addCallParameters( ) ``` -The function returns the calldata as well as the value required to execute the transaction: +The `MintOptions` interface requires three keys: + +- `recipient` defines the address of the Position owner, so in our case the address of our wallet. +- `deadline` defines the latest point in time at which we want our transaction to be included in the blockchain. +- `slippageTolerance` defines the maximum amount of **change of the ratio** of the Tokens we provide. The ratio can change if for example **trades** that change the price of the Pool are included before our transaction. + +The `addCallParameters` function returns the calldata as well as the value required to execute the transaction: ```typescript const transaction = { diff --git a/docs/sdk/v3/guides/liquidity/03-fetching-positions.md b/docs/sdk/v3/guides/liquidity/03-fetching-positions.md index 39542b5c66..744ecb6806 100644 --- a/docs/sdk/v3/guides/liquidity/03-fetching-positions.md +++ b/docs/sdk/v3/guides/liquidity/03-fetching-positions.md @@ -3,15 +3,29 @@ id: fetching-positions title: Fetching Positions --- -# Introduction +## Introduction -## Fetching Positions +This guide will cover how to create (or mint) a liquidity position on the Uniswap V3 protocol. +Like the [Liquidity Position guide](./01-position-data.md) it doesn't have an accompanying example, nevertheless the concepts and functions used here can be found among the various examples that interact with liquidity positions. -The [NonfungiblePositionManager Contract](../../../../contracts/v3/reference/periphery/NonfungiblePositionManager.md) can be used to create Positions, as well as get information on existing Positions. +:::info +If you need an introduction to liquidity positions, check out the [Liquidity Position guide](./01-position-data.md) +::: -In this section we will **fetch all Positions** for an address. +The [NonfungiblePositionManager Contract](../../../../contracts/v3/reference/periphery/NonfungiblePositionManager.md) can be used to create Positions, as well as get information on **existing Positions**. +In this guide, we will fetch **all Positions** an address has and fetch the **detailed Position Data** for those positions. -### Creating an ethers Contract +The guide will **cover**: + +1. Creating an ethersJS contract to interact with the NonfungiblePositionManager. +2. Fetching all positions for an address. +3. Fetching the position info for the positions. + +At the end of the guide, given the inputs above, we should be able to mint a liquidity position with the press of a button and view the position on the UI of the web application. + +For this guide, we do not need to use the Uniswap SDKs, we will only import the contract ABI for the NonfungiblePositionManager Contract from [`@uniswap/v3-periphery`](https://www.npmjs.com/package/@uniswap/v3-periphery). + +## Connecting to the NFTPositionManager Contract We use **ethersJS** to interact with the NonfungiblePositionManager Contract. Let's create an ethers Contract: @@ -30,7 +44,7 @@ const nfpmContract = new ethers.Contract( We get the Contract ABI from the 'v3-periphery` package and the contract address from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md) -### Fetching the Position Ids +## Fetching the Position Ids We want to fetch all Position Ids for our address. We first fetch the number of positions and then the ids by their indices. @@ -55,7 +69,7 @@ for (let i = 0; i < numPositions; i++) { const positionIds = await Promise.all(calls) ``` -### Fetching the Position Info +## Fetching the Position Info Now that we have the ids of the Positions associated with our address, we can fetch the position info using the `positions` function. diff --git a/docs/sdk/v3/guides/liquidity/04-modifying-position.md b/docs/sdk/v3/guides/liquidity/04-modifying-position.md index 017a6198fb..4e349321cd 100644 --- a/docs/sdk/v3/guides/liquidity/04-modifying-position.md +++ b/docs/sdk/v3/guides/liquidity/04-modifying-position.md @@ -33,6 +33,39 @@ This guide assumes you are familiar with our [Minting a Position](./02-minting-p Also note that we do not need to give approval to the `NonfungiblePositionManager` to transfer our tokens as we will have already done that when minting our position. ::: +## Configuration and utils + +The example can be configured in the [`config.ts`](https://github.com/Uniswap/examples/blob/d34a53412dbf905802da2249391788a225719bb8/v3-sdk/modifying-position/src/config.ts) file. +The `CurrentConfig` object has this structure: + +```typescript +export const CurrentConfig: ExampleConfig = { + env: Environment.LOCAL, + rpc: { + local: 'http://localhost:8545', + mainnet: 'https://mainnet.infura.io/v3/0ac57a06f2994538829c14745750d721', + }, + wallet: { + address: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + privateKey: + '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', + }, + tokens: { + token0: USDC_TOKEN, + token0Amount: 1000, + token1: DAI_TOKEN, + token1Amount: 1000, + poolFee: FeeAmount.LOW, + fractionToRemove: 1, + fractionToAdd: 0.5, + }, +} +``` + +You should already be familiar with the `rpc`, `wallet` and token parameters, they are used in the same way as in the guides earlier in our v3-sdk series. +The `fractionToAdd` variable is the multiplicator by which we will increase the Position. A fraction of **0.5** means we increase the liquidity by **50%**. +The `fractionToRemove` variable is the fraction of the Position that we want to remove later in the guide. A fraction of **1** means we remove **100%** of the liquidity. + ## Adding liquidity to our position Assuming we have already minted a position, our first step is to construct the modified position using our original position to calculate the amount by which we want to increase our current position: @@ -103,6 +136,8 @@ const addLiquidityOptions: AddLiquidityOptions = { ``` Compared to minting, we have we have omitted the `recipient` parameter and instead passed in the `tokenId` of the position we previously minted. +As the Position already exists, the recipient doesn't change, instead the NonfungiblePositionManager contract can modify the existing Position by accessing it with its id. + The tokenId can be fetched with the tokenOfOwnerByIndex function of the NonfungiblePositionManager Contract as described [here](./01-position-data.md#fetching-positions). The newly created position along with the options object are then passed to the `NonfungiblePositionManager`'s `addCallParameters`: @@ -110,6 +145,8 @@ The newly created position along with the options object are then passed to the ```typescript import { NonfungiblePositionManager } from '@uniswap/v3-sdk' +const positionToIncreaseBy = constructPosition(CurrentConfig.tokens.amount0, CurrentConfig.tokens.amount1) + const { calldata, value } = NonfungiblePositionManager.addCallParameters( positionToIncreaseBy, addLiquidityOptions diff --git a/docs/sdk/v3/guides/liquidity/05-collecting-fees.md b/docs/sdk/v3/guides/liquidity/05-collecting-fees.md index 06ac92b732..b2860165ab 100644 --- a/docs/sdk/v3/guides/liquidity/05-collecting-fees.md +++ b/docs/sdk/v3/guides/liquidity/05-collecting-fees.md @@ -73,9 +73,30 @@ Similar to the other functions exposed by the `NonfungiblePositionManager`, we p The other two `CurrencyAmount` parameters (`expectedCurrencyOwed0` and `expectedCurrencyOwed1`) define the **maximum** amount of currency we expect to get collect through accrued fees of each token in the pool. We set these through our guide's configuration. +In a real world scenario, we can fetch the amount of fees that are owed to the Position through the `positions()` function of the NonfungiblePositionManager Contract. +We fetch the position info like in this code snippet taken from the [Fetching Positions guide](./03-fetching-positions.md): + +```typescript +const positionInfos = callResponses.map((position) => { + return { + tickLower: position.tickLower, + tickUpper: position.tickUpper, + liquidity: JSBI.BigInt(position.liquidity), + feeGrowthInside0LastX128: JSBI.BigInt(position.feeGrowthInside0LastX128), + feeGrowthInside1LastX128: JSBI.BigInt(position.feeGrowthInside1LastX128), + tokensOwed0: JSBI.BigInt(position.tokensOwed0), + tokensOwed1: JSBI.BigInt(position.tokensOwed1), + } +}) +``` + +The `tokensOwed0` and `tokensOwed1` values are the fees owed. + +In this example, we have the values hardcoded in the [`config.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/collecting-fees/src/config.ts) file. + ## Submitting our fee collection transaction -We then get the call parameters for collecting our fees from our `NonfungiblePositionManager` using the constructed `CollectOptions`: +Next, we get the call parameters for collecting our fees from our `NonfungiblePositionManager` using the constructed `CollectOptions`: ```typescript const { calldata, value } = From b9d1c63502e337fe0113a27af04f2ed27c3604a0 Mon Sep 17 00:00:00 2001 From: Koray Koska <11356621+koraykoska@users.noreply.github.com> Date: Fri, 25 Aug 2023 13:07:36 +0200 Subject: [PATCH 32/52] feat: local development postman workspace --- docs/sdk/v3/guides/02-local-development.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sdk/v3/guides/02-local-development.md b/docs/sdk/v3/guides/02-local-development.md index 0ea355dcbf..47e90e95d6 100644 --- a/docs/sdk/v3/guides/02-local-development.md +++ b/docs/sdk/v3/guides/02-local-development.md @@ -114,6 +114,8 @@ The result should look like the below (see image below as well): As you can see, the chain id is `1`, just like on Mainnet! +You can find the above example and more in [this Postman workspace](https://www.postman.com/chainnodes/workspace/uniswap-examples) under "Local Development". + ## Using your Mainnet Fork Now that you have a running Mainnet Fork, you will be able to use it everywhere in your development setup. From 57b650e8bb814b31e638e944c730b03332c87851 Mon Sep 17 00:00:00 2001 From: Koray Koska <11356621+koraykoska@users.noreply.github.com> Date: Fri, 25 Aug 2023 16:42:30 +0200 Subject: [PATCH 33/52] feat: web3 development basics guide --- .../contracts/v3/guides/local-environment.mdx | 42 ++--- docs/sdk/swap-widget/guides/swap-widget.mdx | 10 +- .../v3/guides/03-web3-development-basics.md | 156 ++++++++++++++++++ docs/sdk/v3/guides/advanced/02-pool-data.md | 2 +- 4 files changed, 180 insertions(+), 30 deletions(-) create mode 100644 docs/sdk/v3/guides/03-web3-development-basics.md diff --git a/docs/contracts/v3/guides/local-environment.mdx b/docs/contracts/v3/guides/local-environment.mdx index bae7c802c3..7884377312 100644 --- a/docs/contracts/v3/guides/local-environment.mdx +++ b/docs/contracts/v3/guides/local-environment.mdx @@ -8,33 +8,33 @@ One of the most common questions we get asked is what development toolset to use At the end of this guide you’ll have a development environment set up that you can use to build the rest of the examples in the Guides section of the docs, or start your own integration project! -To get you started as quickly as possible, we have provided the `Quick Start` section below where you can clone some boiler plate and get building. To start from scratch and learn the underlying concepts, jump to the `Start from Scratch` section. +To get you started as quickly as possible, we have provided the `Quick Start` section below where you can clone some boiler plate and get building. To start from scratch and learn the underlying concepts, jump to the `Start from Scratch` section. # Quick Start -The Uniswap [boilerplate repo](https://github.com/Uniswap/uniswap-first-contract-example) provides a basic Hardhat environment with required imports already pre-loaded for you. You can simply clone it and install the dependencies: +The Uniswap [boilerplate repo](https://github.com/Uniswap/uniswap-first-contract-example) provides a basic Hardhat environment with required imports already pre-loaded for you. You can simply clone it and install the dependencies: ```bash git clone https://github.com/Uniswap/uniswap-first-contract-example cd uniswap-first-contract-example -npm install +npm install ``` -Then hop to the `Local Node with a Mainnet Fork` to complete your set up and start developing. +Then hop to the `Local Node with a Mainnet Fork` to complete your set up and start developing. # Start from Scratch -In the following sections, we’ll walk through the steps to create the same environment set up as the boiler plate from scratch and learn the underlying concepts. +In the following sections, we’ll walk through the steps to create the same environment set up as the boiler plate from scratch and learn the underlying concepts. ## Set Up Dependencies -Node is one of the most common Javascript runtimes. For our purposes it will provide scripting we can use to compile and test our contracts. If you haven’t already, install NodeJS and its package manager NPM ([instructions](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)). Once those dependencies are set up, we can initialize our project: +Node is one of the most common Javascript runtimes. For our purposes it will provide scripting we can use to compile and test our contracts. If you haven’t already, install NodeJS and its package manager NPM ([instructions](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)). Once those dependencies are set up, we can initialize our project: ```bash $ npm init ``` -[Hardhat](https://hardhat.org/) is an Ethereum development toolset that provides a number of powerful features including Solidity compilation, testing and deployment, all in a single convenient wrapper. We’ll use NPM to add Hardhat to our project: +[Hardhat](https://hardhat.org/) is an Ethereum development toolset that provides a number of powerful features including Solidity compilation, testing and deployment, all in a single convenient wrapper. We’ll use NPM to add Hardhat to our project: ```bash $ npm add --save-dev hardhat @@ -54,7 +54,7 @@ Next we’ll use NPM to add the Uniswap V3 contracts which will allow us to seam $ npm add @uniswap/v3-periphery @uniswap/v3-core ``` -The Uniswap V3 contracts were written using a past version of the solidity compiler. Since we’re building integrations on V3 we have to tell Hardhat to use the correct compiler to build these files. Go to the `./hardhat.config.js` file and change the Solidity version to “0.7.6”: +The Uniswap V3 contracts were written using a past version of the solidity compiler. Since we’re building integrations on V3 we have to tell Hardhat to use the correct compiler to build these files. Go to the `./hardhat.config.js` file and change the Solidity version to “0.7.6”: ```jsx // ... @@ -63,11 +63,11 @@ module.exports = { }; ``` -That’s it! You should now have a functional development environment to start building on chain Uniswap integrations. Let’s run a quick test to confirm everything is set up properly. +That’s it! You should now have a functional development environment to start building on chain Uniswap integrations. Let’s run a quick test to confirm everything is set up properly. ## Compile a Basic Contract -To confirm that our environment is configured correctly we’ll attempt to compile a basic Swap contract. Create a new file, `./contracts/Swap.sol` and paste the following code into it (a detailed guide to this contract can be found [here](./swaps/single-swaps)): +To confirm that our environment is configured correctly we’ll attempt to compile a basic Swap contract. Create a new file, `./contracts/Swap.sol` and paste the following code into it (a detailed guide to this contract can be found [here](./swaps/single-swaps)): ```jsx // SPDX-License-Identifier: GPL-2.0-or-later @@ -82,11 +82,11 @@ contract SimpleSwap { address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; address public constant WETH9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; uint24 public constant feeTier = 3000; - + constructor(ISwapRouter _swapRouter) { swapRouter = _swapRouter; } - + function swapWETHForDAI(uint256 amountIn) external returns (uint256 amountOut) { // Transfer the specified amount of WETH9 to this contract. @@ -114,13 +114,13 @@ contract SimpleSwap { } ``` -To compile all the contracts in the `./contracts` folder, we’ll use the Hardhat compile command: +To compile all the contracts in the `./contracts` folder, we’ll use the Hardhat compile command: ```bash $ npx hardhat compile ``` -If the environment is compiled correctly you should see the message: +If the environment is compiled correctly you should see the message: ```bash Compiled { x } Solidity files successfully @@ -128,17 +128,11 @@ Compiled { x } Solidity files successfully # Local Node with a Mainnet Fork -When building and testing integrations with on chain protocols, developers often hit a problem: the liquidity on the live chain is critical to thoroughly testing their code but testing against a live network like Mainnet can be extremely expensive. - -Luckily, Hardhat has a powerful feature that allows developers to run a local Ethereum test node that uses a fork of Mainnet. This allows us to test against simulated liquidity for free. +When building and testing integrations with on chain protocols, developers often hit a problem: the liquidity on the live chain is critical to thoroughly testing their code but testing against a live network like Mainnet can be extremely expensive. -As a prerequisite we’ll need an RPC that supports [Forking](https://hardhat.org/hardhat-network/docs/guides/forking-other-networks). [Alchemy](https://www.alchemy.com/) includes forking in its free tier so it’s a great place to start (sign up and get an API key [here](https://www.alchemy.com/)). You can then run the following Hardhat command to start your node: - -```bash -$ npx hardhat node --fork https://eth-mainnet.alchemyapi.io/v2/{YOUR_API_KEY} -``` +See [the SDK getting started guide](../../../sdk/v3/guides/02-local-development) for a full example on how to use forks. -With your local node up and running, you can use the `--network localhost` flag in tests to point the Hardhat testing suite to that local node: +With your local node up and running, you can use the `--network localhost` flag in tests to point the Hardhat testing suite to that local node: ```bash $ npx hardhat test --network localhost @@ -146,7 +140,7 @@ $ npx hardhat test --network localhost # Next Steps -With your environment set up you’re ready to start building. Jump over to the guides section to learn about the Uniswap functions you can integrate with. Remember to add all contracts (.sol files) to the `./contracts` folder and their subsequent tests to the `./tests` folder. You can then test them against your local forked node by running: +With your environment set up you’re ready to start building. Jump over to the guides section to learn about the Uniswap functions you can integrate with. Remember to add all contracts (.sol files) to the `./contracts` folder and their subsequent tests to the `./tests` folder. You can then test them against your local forked node by running: ```bash $ npx hardhat test --network localhost diff --git a/docs/sdk/swap-widget/guides/swap-widget.mdx b/docs/sdk/swap-widget/guides/swap-widget.mdx index 982f533b84..2bba552dcb 100644 --- a/docs/sdk/swap-widget/guides/swap-widget.mdx +++ b/docs/sdk/swap-widget/guides/swap-widget.mdx @@ -71,7 +71,7 @@ import '@uniswap/widgets/dist/fonts.css' The swap widget is a React component that can be easily imported into any React project. It's designed to work out-of-the-box, with no required parameters or web3 integration necessary. Trading is supported on all networks where the Uniswap Protocol is deployed. -Although the widget will work out-of-the-box with no parameters, we highly recommend integrators provide [JSON-RPC endpoints](#json-rpc-endpoint), like from Infura or Alchemy. Existing dApps can also provide their existing [web3 provider](#web3-provider) to integrate the widget seamlessly into their dApp. +Although the widget will work out-of-the-box with no parameters, we highly recommend integrators provide [JSON-RPC endpoints](#json-rpc-endpoint), like from [Chainnodes](https://www.chainnodes.org), Infura or Alchemy. Existing dApps can also provide their existing [web3 provider](#web3-provider) to integrate the widget seamlessly into their dApp. Additional [optional parameters](/sdk/swap-widget/reference/v2#optional-parameters) are available to customize the appearance and performance of the swap widget to fit your unique use case. @@ -100,8 +100,8 @@ import '@uniswap/widgets/fonts.css' import { provider } from './your/provider' // We recommend you pass your own JSON-RPC endpoints. -const jsonRpcUrlMap = { - 1: ['https://mainnet.infura.io/v3/'], +const jsonRpcUrlMap = { + 1: ['https://mainnet.infura.io/v3/'], 3: ['https://ropsten.infura.io/v3/'] } @@ -116,9 +116,9 @@ function App() { JSON-RPC endpoints are used to read data when no `provider` is connected. We strongly recommend you pass either a Web3 Provider to the `provider` prop, or JSON-RPC endpoint URLs to the `jsonRpcUrlMap` prop. -The widget will use these endpoints to fetch on-chain data and submit transactions for signature. If the user connects a MetaMask wallet, the widget will use the JSON-RPC provided by MetaMask when possible. [(See a list of all chains supported on widget.)](https://github.com/Uniswap/widgets/blob/main/src/constants/chains.ts#L4) +The widget will use these endpoints to fetch on-chain data and submit transactions for signature. If the user connects a MetaMask wallet, the widget will use the JSON-RPC provided by MetaMask when possible. [(See a list of all chains supported on widget.)](https://github.com/Uniswap/widgets/blob/main/src/constants/chains.ts#L4) -If you don’t yet have JSON-RPC endpoints, you can easily create them with services like [Infura](https://infura.io/product/ethereum) or [Alchemy](https://www.alchemy.com/supernode). +If you don’t yet have JSON-RPC endpoints, you can easily create them with services like [Chainnodes](https://www.chainnodes.org), [Infura](https://infura.io/product/ethereum) or [Alchemy](https://www.alchemy.com/supernode). If you choose not to provide a `jsonRpcUrlMap` prop or are missing endpoints for some chains, the widget uses free public JSON-RPC endpoints to still allow users to interact with the widget and fetch price quotes. However, these public endpoints are NOT recommended for production environment usage, are severely rate-limited, and aren't necessarily as reliable. When possible, we'd recommend providing your own endpoints! diff --git a/docs/sdk/v3/guides/03-web3-development-basics.md b/docs/sdk/v3/guides/03-web3-development-basics.md new file mode 100644 index 0000000000..46f47fcb39 --- /dev/null +++ b/docs/sdk/v3/guides/03-web3-development-basics.md @@ -0,0 +1,156 @@ +--- +id: web3-development-basics +title: Web3 Development Basics +--- + +## Introduction + +Developing dApps and interacting with Smart Contracts is quite different from Web2, and at times challenging due to little information +on this topic compared to other areas of software development. + +This developer guide is a quick overview of the space, including references to libraries and guides that are great starting points. + +It is assumed that you know the basics about Ethereum and the blockchain, including some terminology. + +## RPCs + +The access point to the blockchain are RPCs. They are the [standardized interface](https://ethereum.org/en/developers/docs/apis/json-rpc/) +to read data from smart contracts, send transactions and interact with on-chain protocols. + +RPCs are basically [full or archival nodes](https://ethereum.org/en/developers/docs/nodes-and-clients/archive-nodes/) +with an interface ([JSON-RPC](https://www.jsonrpc.org/)). + +To support Ethereum's decentralization, once can host a node themselves using one of the implementations listed below: + +* [geth](https://github.com/ethereum/go-ethereum) - The original (reference) implementation of the Ethereum protocol +* [erigon](https://github.com/ledgerwatch/erigon) - A very efficient archival node implementation +* [Nethermind](https://github.com/NethermindEth/nethermind) - An Ethereum implementation focused on stability + +As achieving high availability and making sure your node is synced up all the time turns out to be quite the challenge, +there are nodes as a service (RPC) providers that you can use, especially in production environments. +[Chainnodes](https://www.chainnodes.org/) is a robust RPC provider with generous free tier that you can use in both +development and production environments. + +### JSON-RPC Standard + +RPCs communicate over the [JSON-RPC](https://www.jsonrpc.org/) standard. To send requests, you take the RPC URL and +make a POST request with a JSON body. See the below example: + +`POST https://mainnet.chainnodes.org/API_KEY` + +Body: + +```json +{ + "jsonrpc": "2.0", + "method": "eth_blockNumber", + "params": [], + "id": 1 +} +``` + +This request would respond with the following: + +```json + +``` + +You can find the above examples, including more, in [this Postman collection](https://www.postman.com/chainnodes/workspace/uniswap-examples). + +To check out all possible RPC requests, head over to the [Chainnodes Docs](https://www.chainnodes.org/docs). + +### Client implementations + +As communicating over HTTP with POST requests directly can be quite difficult, especially if you want to properly +encode and decode responses and handle failures and exponential backoffs, there are client implementations that +can do the heavy lifting for you. They interact as SDKs that have developer friendly APIs and internally handle +creating the proper RPC requests, sending them to the endpoint you choose and decoding the response for you. + +Some of the major implementations are listed below: + +* [ethers.js](https://github.com/ethers-io/ethers.js) - Javascript/Typescript SDK for NodeJS and the Browser. Used throughout the Uniswap Docs. +* [wagmi](https://github.com/wagmi-dev/wagmi) and [viem](https://github.com/wagmi-dev/viem) - Javascript/Typescript, great duo for modern Web3 development in the Browser. +* [Web3.swift](https://github.com/Boilertalk/Web3.swift) - Swift SDK for iOS apps and Backends. +* [KEthereum](https://github.com/komputing/KEthereum) - Kotlin SDK for Android development. +* [ethers-rs](https://github.com/gakonst/ethers-rs) - Rust SDK. +* [ethclient](https://github.com/ethereum/go-ethereum/tree/master/ethclient) - Go SDK, part of geth, the reference Ethereum node implementation. + +As you can see there are lots of SDKs to make it easier to communicate via RPC with the blockchain. + +### Local Development + +To simulate RPCs and transactions locally, you can check [this guide](./02-local-development). + +## Indexers + +As RPCs are only a slim abstraction of the database that stores the blockchain data, there are certain things that you cannot +access easily via RPCs. + +One typical example is transactions of a specific Wallet address. Imagine you want to get a list of all transactions that originated from (or to) a +specific Wallet. One could think there should be an RPC method called `eth_getTransactionsForWallet` or something similar. +But due to the nature of how the data is stored, this RPC method is not feasible, and hence not implemented. + +Now, instead of accessing those kind of things by iterating through every block in the blockchain, you can use Indexers, that are +designed to index data like that on the go and provide easy access to it. + +### TheGraph + +A well-known, standardized implementation of indexers is [TheGraph](https://thegraph.com/). It is used by several of the big protocols, +including Uniswap, to index data and make it accessible to users and dashboards. + +Using TheGraph, you can either access open [subgraphs](https://thegraph.com/explorer) via the GraphQL querying language, or [create your own](https://thegraph.com/docs/en/developing/creating-a-subgraph/) and deploy it. + +Throughout the docs you will see how to interact with the Uniswap subgraph to fetch tick data and more without ever touching RPCs. + +An important note about Indexers though: + +While they can be helpful, you need to be aware that the ultimate source of truth comes from the RPCs. As reorgs happen and +certain issues on indexers arise, there might be certain datapoints that are either not fully up-to-date or even +completely wrong on Indexers. If you have a use-case that requires perfectly correct data all the time, use RPCs directly. +If you are just doing data visualization or dashboards, use Indexers if they fit your use-case. + +Some popular subgraphs that you can try to fetch data from the blockchain easily (click on playground to give it a try): + +* [Uniswap Messari subgraph](https://thegraph.com/explorer/subgraphs/ELUcwgpm14LKPLrBRuVvPvNKHQ9HvwmtKgKSH6123cr7?view=Overview&chain=mainnet) +* [Snapshot Subgraph](https://thegraph.com/explorer/subgraphs/3Q4vnuSqemXnSNHoiLD7wdBbGCXszUYnUbTz191kDMNn?view=Overview&chain=mainnet) +* [Aave V3 Messari subgraph](https://thegraph.com/explorer/subgraphs/HB1Z2EAw4rtPRYVb2Nz8QGFLHCpym6ByBX6vbCViuE9F?view=Overview&chain=mainnet) + +## Smart Contract Development + +Smart contracts are typically developed using the [Solidity language](https://soliditylang.org/). + +There are VSCode plugins that make the development with Solidity easier. One of them is Juan Blanco's "Solidity" that you can find +over [here](https://marketplace.visualstudio.com/items?itemName=JuanBlanco.solidity). + +You would typically also go for a developer suite like one of the following: + +* [Foundry](https://github.com/foundry-rs/foundry) - Fast and modern developer tools for smart contract engineers. +* [Hardhat](https://github.com/NomicFoundation/hardhat) - Scriptable, easy and battle-tested JS developer suite for smart contracts. +* (deprecated) Ganache - One of the original developer suites. Should be avoided as there are many issues with it. + +To read data from smart contracts, or interact with them, use the [client SDKs](#client-implementations) linked to above. +They help you generate the necessary RPC calls to fetch data from / send transactions to the blockchain to interact with your +smart contracts. You can read more about it in the [ethers.js docs](https://docs.ethers.org/v5/api/contract/contract/). + +## The Uniswap SDK + +How does the Uniswap SDK fit into the stack explained above? + +The Uniswap SDK is a tool that makes it easy to get quotes for swaps, simulate swaps and actually send transactions that execute a swap +on-chain. + +As you know already, all interactions with the blockchain happens through RPCs. So the Uniswap SDK, as you will see throughput the guides, +requires you to have access to an RPC endpoint like [Chainnodes](https://www.chainnodes.org). +When reading data, the data is read from the given RPC endpoint. When actually swapping, you will need to sign a transaction +using a private key. + +On top of that we strive to make it easier in general to interact with Uniswap or build on top of it. So using the SDK +you will be able to create Pools, [create LP Positions](./liquidity/01-position-data), [Swap](./swaps/01-quoting.md), Fetch Tick Data and display them, and more. + +## Next Steps + +Go through the basic guides first and try to fetch some data and interact with the Uniswap ecosystem a little bit. You can even +[send your first swap transaction](./swaps/02-trading) on a local fork! + +While some concepts in Web3 require thinking outside of the box, this guide should give you a good overview on where to start. +You should now be fully equipped to follow our other guides. diff --git a/docs/sdk/v3/guides/advanced/02-pool-data.md b/docs/sdk/v3/guides/advanced/02-pool-data.md index 4e08a12124..cbcdd0e260 100644 --- a/docs/sdk/v3/guides/advanced/02-pool-data.md +++ b/docs/sdk/v3/guides/advanced/02-pool-data.md @@ -96,7 +96,7 @@ Uniswap V3 allows 4 different Fee tiers when deploying a pool, so multiple pools ## Creating a Pool Contract instance and fetching metadata Now that we have the address of a **USDC - ETH** Pool, we can construct an instance of an **ethers** `Contract` to interact with it. -To construct the Contract we need to provide the address of the contract, its ABI and a provider connected to an [RPC endpoint](https://docs.infura.io/infura/getting-started). We get access to the contract's ABI through the `@uniswap/v3-core` package, which holds the core smart contracts of the Uniswap V3 protocol: +To construct the Contract we need to provide the address of the contract, its ABI and a provider connected to an [RPC endpoint](https://www.chainnodes.org/docs). We get access to the contract's ABI through the `@uniswap/v3-core` package, which holds the core smart contracts of the Uniswap V3 protocol: ```typescript import { ethers } from 'ethers From fa240fa8f5f4647a886961b1c835ee13fd6369c8 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Mon, 28 Aug 2023 17:26:55 +0100 Subject: [PATCH 34/52] Correct order of guides in v3-sdk section --- docs/sdk/v3/guides/02-local-development.md | 1 + docs/sdk/v3/guides/03-web3-development-basics.md | 1 + docs/sdk/v3/guides/advanced/_category_.json | 2 +- docs/sdk/v3/guides/liquidity/_category_.json | 2 +- docs/sdk/v3/guides/swaps/_category_.json | 2 +- 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/sdk/v3/guides/02-local-development.md b/docs/sdk/v3/guides/02-local-development.md index 47e90e95d6..fd1808604c 100644 --- a/docs/sdk/v3/guides/02-local-development.md +++ b/docs/sdk/v3/guides/02-local-development.md @@ -1,6 +1,7 @@ --- id: local-development title: Local Development +position: 2 --- ## Introduction diff --git a/docs/sdk/v3/guides/03-web3-development-basics.md b/docs/sdk/v3/guides/03-web3-development-basics.md index 46f47fcb39..4ff7ba220d 100644 --- a/docs/sdk/v3/guides/03-web3-development-basics.md +++ b/docs/sdk/v3/guides/03-web3-development-basics.md @@ -1,6 +1,7 @@ --- id: web3-development-basics title: Web3 Development Basics +position: 3 --- ## Introduction diff --git a/docs/sdk/v3/guides/advanced/_category_.json b/docs/sdk/v3/guides/advanced/_category_.json index e190f9f051..65c1e31617 100644 --- a/docs/sdk/v3/guides/advanced/_category_.json +++ b/docs/sdk/v3/guides/advanced/_category_.json @@ -1,5 +1,5 @@ { "label": "Advanced", - "position": 4, + "position": 6, "collapsed": true } diff --git a/docs/sdk/v3/guides/liquidity/_category_.json b/docs/sdk/v3/guides/liquidity/_category_.json index b7bfa33814..1a66cbbe2c 100644 --- a/docs/sdk/v3/guides/liquidity/_category_.json +++ b/docs/sdk/v3/guides/liquidity/_category_.json @@ -1,5 +1,5 @@ { "label": "Pooling Liquidity", - "position": 3, + "position": 5, "collapsed": true } diff --git a/docs/sdk/v3/guides/swaps/_category_.json b/docs/sdk/v3/guides/swaps/_category_.json index 16e66adb14..145573eb19 100644 --- a/docs/sdk/v3/guides/swaps/_category_.json +++ b/docs/sdk/v3/guides/swaps/_category_.json @@ -1,5 +1,5 @@ { "label": "Swaps", - "position": 2, + "position": 4, "collapsed": true } From d2616d926ed7b0d53422540a14766e9c4330a213 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Mon, 18 Sep 2023 18:31:22 +0100 Subject: [PATCH 35/52] Improve web3-dev basics guide --- docs/sdk/swap-widget/guides/swap-widget.mdx | 4 +- .../v3/guides/03-web3-development-basics.md | 91 +++++++++++++------ 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/docs/sdk/swap-widget/guides/swap-widget.mdx b/docs/sdk/swap-widget/guides/swap-widget.mdx index 2bba552dcb..891b8f0e8e 100644 --- a/docs/sdk/swap-widget/guides/swap-widget.mdx +++ b/docs/sdk/swap-widget/guides/swap-widget.mdx @@ -101,7 +101,7 @@ import { provider } from './your/provider' // We recommend you pass your own JSON-RPC endpoints. const jsonRpcUrlMap = { - 1: ['https://mainnet.infura.io/v3/'], + 1: ['https://mainnet.infura.io/v3/'], 3: ['https://ropsten.infura.io/v3/'] } @@ -116,7 +116,7 @@ function App() { JSON-RPC endpoints are used to read data when no `provider` is connected. We strongly recommend you pass either a Web3 Provider to the `provider` prop, or JSON-RPC endpoint URLs to the `jsonRpcUrlMap` prop. -The widget will use these endpoints to fetch on-chain data and submit transactions for signature. If the user connects a MetaMask wallet, the widget will use the JSON-RPC provided by MetaMask when possible. [(See a list of all chains supported on widget.)](https://github.com/Uniswap/widgets/blob/main/src/constants/chains.ts#L4) +The widget will use these endpoints to fetch on-chain data and submit transactions for signature. If the user connects a MetaMask wallet, the widget will use the JSON-RPC provided by MetaMask when possible. [(See a list of all chains supported on widget.)](https://github.com/Uniswap/widgets/blob/main/src/constants/chains.ts#L4) If you don’t yet have JSON-RPC endpoints, you can easily create them with services like [Chainnodes](https://www.chainnodes.org), [Infura](https://infura.io/product/ethereum) or [Alchemy](https://www.alchemy.com/supernode). diff --git a/docs/sdk/v3/guides/03-web3-development-basics.md b/docs/sdk/v3/guides/03-web3-development-basics.md index 4ff7ba220d..a244e676c9 100644 --- a/docs/sdk/v3/guides/03-web3-development-basics.md +++ b/docs/sdk/v3/guides/03-web3-development-basics.md @@ -10,27 +10,32 @@ Developing dApps and interacting with Smart Contracts is quite different from We on this topic compared to other areas of software development. This developer guide is a quick overview of the space, including references to libraries and guides that are great starting points. +Reading this guide should help you identify areas that you might need to learn a bit more about and prepare you for the following Uniswap-specific guides. It is assumed that you know the basics about Ethereum and the blockchain, including some terminology. +If you already know how to build dApps and interact with ethersJS etc. you can safely skip this guide. ## RPCs -The access point to the blockchain are RPCs. They are the [standardized interface](https://ethereum.org/en/developers/docs/apis/json-rpc/) +The access point to the blockchain are RPC nodes. They are the [standardized interface](https://ethereum.org/en/developers/docs/apis/json-rpc/) to read data from smart contracts, send transactions and interact with on-chain protocols. -RPCs are basically [full or archival nodes](https://ethereum.org/en/developers/docs/nodes-and-clients/archive-nodes/) -with an interface ([JSON-RPC](https://www.jsonrpc.org/)). +RPCs are either [full or archival nodes](https://ethereum.org/en/developers/docs/nodes-and-clients/archive-nodes/) +with a ([JSON-RPC](https://www.jsonrpc.org/)) interface. -To support Ethereum's decentralization, once can host a node themselves using one of the implementations listed below: +To support Ethereum's decentralization, one can host a node themselves, for example by using one of the implementations listed below: * [geth](https://github.com/ethereum/go-ethereum) - The original (reference) implementation of the Ethereum protocol * [erigon](https://github.com/ledgerwatch/erigon) - A very efficient archival node implementation * [Nethermind](https://github.com/NethermindEth/nethermind) - An Ethereum implementation focused on stability -As achieving high availability and making sure your node is synced up all the time turns out to be quite the challenge, +As achieving high availability and making sure your node is synced all the time turns out to be quite challenging, there are nodes as a service (RPC) providers that you can use, especially in production environments. -[Chainnodes](https://www.chainnodes.org/) is a robust RPC provider with generous free tier that you can use in both -development and production environments. +When choosing an RPC provider, we suggest you look for an RPC service that supports websockets as they provide far superior performance than HTTP connections. +To ensure interoperability, you should also ensure that your RPC provider adheres strictly to the JSON-RPC standard and doesn't require custom requests. + +[Chainnodes](https://www.chainnodes.org/) is a robust RPC provider with generous free tier that you can use in both development and production environments. +For testing purposes you could also use a free public RPC endpoint, for example from [Chainlist](https://chainlist.org/). ### JSON-RPC Standard @@ -50,12 +55,19 @@ Body: } ``` -This request would respond with the following: +At the time of writing, this request would respond with the following: ```json - +{ + "id": 1, + "result": "0x11527c0", + "jsonrpc": "2.0" +} ``` +Examining the result, we see that the result is an encoded hex string. +After decoding it, we see it returns the current blocknumber of our network, `18163648`. + You can find the above examples, including more, in [this Postman collection](https://www.postman.com/chainnodes/workspace/uniswap-examples). To check out all possible RPC requests, head over to the [Chainnodes Docs](https://www.chainnodes.org/docs). @@ -64,18 +76,24 @@ To check out all possible RPC requests, head over to the [Chainnodes Docs](https As communicating over HTTP with POST requests directly can be quite difficult, especially if you want to properly encode and decode responses and handle failures and exponential backoffs, there are client implementations that -can do the heavy lifting for you. They interact as SDKs that have developer friendly APIs and internally handle +can do the heavy lifting for you. These SDKs have developer friendly APIs and internally handle creating the proper RPC requests, sending them to the endpoint you choose and decoding the response for you. Some of the major implementations are listed below: * [ethers.js](https://github.com/ethers-io/ethers.js) - Javascript/Typescript SDK for NodeJS and the Browser. Used throughout the Uniswap Docs. * [wagmi](https://github.com/wagmi-dev/wagmi) and [viem](https://github.com/wagmi-dev/viem) - Javascript/Typescript, great duo for modern Web3 development in the Browser. +* [web3js](https://github.com/web3/web3.js) - Javascript/Typescript SDK for NodeJS and the Browser by ChainSafe. + +Web3 development is not limited to JS. Web3 libraries for various languages include: + * [Web3.swift](https://github.com/Boilertalk/Web3.swift) - Swift SDK for iOS apps and Backends. * [KEthereum](https://github.com/komputing/KEthereum) - Kotlin SDK for Android development. * [ethers-rs](https://github.com/gakonst/ethers-rs) - Rust SDK. * [ethclient](https://github.com/ethereum/go-ethereum/tree/master/ethclient) - Go SDK, part of geth, the reference Ethereum node implementation. +At the moment, Uniswap only offers Typescript sdks. + As you can see there are lots of SDKs to make it easier to communicate via RPC with the blockchain. ### Local Development @@ -84,11 +102,11 @@ To simulate RPCs and transactions locally, you can check [this guide](./02-local ## Indexers -As RPCs are only a slim abstraction of the database that stores the blockchain data, there are certain things that you cannot -access easily via RPCs. +As RPCs are only a slim abstraction of the data stored in the blockchain, there are certain things that are hard +or expensive to access with regular RPC requests. -One typical example is transactions of a specific Wallet address. Imagine you want to get a list of all transactions that originated from (or to) a -specific Wallet. One could think there should be an RPC method called `eth_getTransactionsForWallet` or something similar. +A common example are transactions of a specific Wallet address. Imagine you want to get a list of all transactions that originated from (or to) a +specific Wallet. One could think there should be an RPC method called `eth_getTransactionsForWallet` or something similar. But due to the nature of how the data is stored, this RPC method is not feasible, and hence not implemented. Now, instead of accessing those kind of things by iterating through every block in the blockchain, you can use Indexers, that are @@ -96,7 +114,7 @@ designed to index data like that on the go and provide easy access to it. ### TheGraph -A well-known, standardized implementation of indexers is [TheGraph](https://thegraph.com/). It is used by several of the big protocols, +A well-known, standardized implementation of indexers is [TheGraph](https://thegraph.com/). It is used by most major protocols, including Uniswap, to index data and make it accessible to users and dashboards. Using TheGraph, you can either access open [subgraphs](https://thegraph.com/explorer) via the GraphQL querying language, or [create your own](https://thegraph.com/docs/en/developing/creating-a-subgraph/) and deploy it. @@ -123,30 +141,47 @@ Smart contracts are typically developed using the [Solidity language](https://so There are VSCode plugins that make the development with Solidity easier. One of them is Juan Blanco's "Solidity" that you can find over [here](https://marketplace.visualstudio.com/items?itemName=JuanBlanco.solidity). -You would typically also go for a developer suite like one of the following: +You would typically also go for a developer suite with testing, compilation and deployment capabilities: * [Foundry](https://github.com/foundry-rs/foundry) - Fast and modern developer tools for smart contract engineers. * [Hardhat](https://github.com/NomicFoundation/hardhat) - Scriptable, easy and battle-tested JS developer suite for smart contracts. -* (deprecated) Ganache - One of the original developer suites. Should be avoided as there are many issues with it. -To read data from smart contracts, or interact with them, use the [client SDKs](#client-implementations) linked to above. -They help you generate the necessary RPC calls to fetch data from / send transactions to the blockchain to interact with your -smart contracts. You can read more about it in the [ethers.js docs](https://docs.ethers.org/v5/api/contract/contract/). +To read data from smart contracts, or interact with them, use the [client SDKs](#client-implementations) mentioned above. +They help you generate the necessary RPC calls to fetch data from / send transactions to the blockchain and interact with your +smart contracts. +You can read more about it in the [ethers.js docs](https://docs.ethers.org/v5/api/contract/contract/). + +## Blockchain Explorers + +Manually gathering information and inspecting data stored in a blockchain is a tedious task. +Almost all chains have at least one accompanying block explorer to help visualize addresses, transactions, contracts and more. + +For Ethereum mainnet, we suggest using [Etherscan](https://etherscan.io/). +You can use Etherscan to inspect [contracts](https://etherscan.io/address/0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45#code), transactions, blocks, and much more. + +If you are looking to debug a transaction, [Tenderly](https://dashboard.tenderly.co/tx/mainnet/0xa4affe1abfaf28d1763d6c3ccda33e717462a928abca89415fc6e661dd7e0c55) can also be a very helpful tool. +In this example of a failed transaction on the Uniswap V2 Router you can easily see why the execution failed and where. -## The Uniswap SDK +## The Uniswap development suite -How does the Uniswap SDK fit into the stack explained above? +Uniswap offers several SDKs that work together and enable you to easily interact with the Uniswap protocol +The most important SDKs are: -The Uniswap SDK is a tool that makes it easy to get quotes for swaps, simulate swaps and actually send transactions that execute a swap -on-chain. +* [sdk-core](https://github.com/Uniswap/sdk-core): The core of the Uniswap SDKs, defines classes and types shared across all the SDKs +* [v2-sdk](https://github.com/Uniswap/v2-sdk): An SDK to interact with the Uniswap V2 protocol. +* [v3-sdk](https://github.com/Uniswap/v3-sdk): An SDK to interact with the Uniswap V3 protocol. +* [router-sdk](https://github.com/Uniswap/router-sdk): Provides abstractions to interact with the (older) SwapRouter contracts. +* [universal-router-sdk](https://github.com/Uniswap/universal-router-sdk): Abstracts interactions with the Universal Router. +* [smart-order-router](https://github.com/Uniswap/smart-order-router): Searches for the most efficient routes for a trade. +* [permit2-sdk](https://github.com/Uniswap/permit2-sdk): Simplifies interactions with Permit2 in JS. +* [uniswapx-sdk](https://github.com/Uniswap/uniswapx-sdk): SDK for the UniswapX protocol. -As you know already, all interactions with the blockchain happens through RPCs. So the Uniswap SDK, as you will see throughput the guides, -requires you to have access to an RPC endpoint like [Chainnodes](https://www.chainnodes.org). +As you know already, all interactions with the blockchain happens through RPCs. So the Uniswap SDKs, as you will see throughout the guides, +requires you to have access to an RPC endpoint like [Chainnodes](https://www.chainnodes.org). When reading data, the data is read from the given RPC endpoint. When actually swapping, you will need to sign a transaction using a private key. -On top of that we strive to make it easier in general to interact with Uniswap or build on top of it. So using the SDK -you will be able to create Pools, [create LP Positions](./liquidity/01-position-data), [Swap](./swaps/01-quoting.md), Fetch Tick Data and display them, and more. +We are continuously working on improving the Uniswap development suite, so stay tuned for more updates. ## Next Steps From e94a43237797ef701397c163a87b49deff2ecdb6 Mon Sep 17 00:00:00 2001 From: Koray Koska <11356621+koraykoska@users.noreply.github.com> Date: Sun, 29 Oct 2023 19:18:24 +0100 Subject: [PATCH 36/52] fix: broken link redirects --- docs/sdk/swap-widget/guides/swap-widget.mdx | 6 +++--- docusaurus.config.js | 6 +++--- src/pages/index.tsx | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/sdk/swap-widget/guides/swap-widget.mdx b/docs/sdk/swap-widget/guides/swap-widget.mdx index 891b8f0e8e..6c04c46b4d 100644 --- a/docs/sdk/swap-widget/guides/swap-widget.mdx +++ b/docs/sdk/swap-widget/guides/swap-widget.mdx @@ -53,7 +53,7 @@ function App() { } ``` -That’s it! You should now see a fully functional swap widget on your site. The widget is self-contained and gracefully handles all interactions with the Uniswap Protocol. It leverages the [Auto Router](/sdk/v3/guides/routing) to compute the best price across all Uniswap v2 and v3 pools. +That’s it! You should now see a fully functional swap widget on your site. The widget is self-contained and gracefully handles all interactions with the Uniswap Protocol. It leverages the [Auto Router](/sdk/v3/guides/swaps/routing) to compute the best price across all Uniswap v2 and v3 pools. See a full implementation of the swap widget in the `/cra` and `/nextjs` branches of the [widgets-demo](https://github.com/Uniswap/widgets-demo) repo. @@ -101,7 +101,7 @@ import { provider } from './your/provider' // We recommend you pass your own JSON-RPC endpoints. const jsonRpcUrlMap = { - 1: ['https://mainnet.infura.io/v3/'], + 1: ['https://mainnet.infura.io/v3/'], 3: ['https://ropsten.infura.io/v3/'] } @@ -116,7 +116,7 @@ function App() { JSON-RPC endpoints are used to read data when no `provider` is connected. We strongly recommend you pass either a Web3 Provider to the `provider` prop, or JSON-RPC endpoint URLs to the `jsonRpcUrlMap` prop. -The widget will use these endpoints to fetch on-chain data and submit transactions for signature. If the user connects a MetaMask wallet, the widget will use the JSON-RPC provided by MetaMask when possible. [(See a list of all chains supported on widget.)](https://github.com/Uniswap/widgets/blob/main/src/constants/chains.ts#L4) +The widget will use these endpoints to fetch on-chain data and submit transactions for signature. If the user connects a MetaMask wallet, the widget will use the JSON-RPC provided by MetaMask when possible. [(See a list of all chains supported on widget.)](https://github.com/Uniswap/widgets/blob/main/src/constants/chains.ts#L4) If you don’t yet have JSON-RPC endpoints, you can easily create them with services like [Chainnodes](https://www.chainnodes.org), [Infura](https://infura.io/product/ethereum) or [Alchemy](https://www.alchemy.com/supernode). diff --git a/docusaurus.config.js b/docusaurus.config.js index 723a621971..4d97703211 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -240,15 +240,15 @@ module.exports = { from: '/sdk/v3/guides/quick-start', }, { - to: '/sdk/v3/guides/quoting', + to: '/sdk/v3/guides/swaps/quoting', from: ['/sdk/v3/guides/creating-a-pool', '/sdk/v3/guides/fetching-prices'], }, { - to: '/sdk/v3/guides/trading', + to: '/sdk/v3/guides/swaps/trading', from: '/sdk/v3/guides/creating-a-trade', }, { - to: '/sdk/v3/guides/routing', + to: '/sdk/v3/guides/swaps/routing', from: '/sdk/v3/guides/auto-router', }, { diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 2cbc8ea89c..4c11f05f69 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -79,12 +79,12 @@ export const dAppGuides = [ { title: 'Create a Trade', text: 'Fetch a Quote for a Trade and execute the Trade', - to: '/sdk/v3/guides/trading', + to: '/sdk/v3/guides/swaps/trading', }, { title: 'Route trades', text: 'Use Routing to get optimized prices for your Trades', - to: '/sdk/v3/guides/routing', + to: '/sdk/v3/guides/swaps/routing', }, { title: 'Provide liquidity', From d420b9f4b0eaab4b2ae03e9486343fa0cbf55be4 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Thu, 9 Nov 2023 23:41:39 +0100 Subject: [PATCH 37/52] Update single hop quoting guide --- docs/sdk/v3/guides/swaps/01-quoting.md | 152 ++++++------------------- docs/sdk/v3/guides/swaps/02-trading.md | 2 +- 2 files changed, 36 insertions(+), 118 deletions(-) diff --git a/docs/sdk/v3/guides/swaps/01-quoting.md b/docs/sdk/v3/guides/swaps/01-quoting.md index f858bacd25..da67cde234 100644 --- a/docs/sdk/v3/guides/swaps/01-quoting.md +++ b/docs/sdk/v3/guides/swaps/01-quoting.md @@ -11,16 +11,14 @@ This guide will cover how to get the current quotes for any token pair on the Un If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](../01-background.md) page! ::: -In this example we will use `quoteExactInputSingle` to get a quote for the pair **USDC - WETH**. +In this example we will use the Quoter class to get a quote for a trade from **USDC to WETH**. The inputs are the **token in**, the **token out**, the **amount in** and the **fee**. -The **fee** input parameters represents the swap fee that distributed to all in range liquidity at the time of the swap. It is one of the identifiers of a Pool, the others being **tokenIn** and **tokenOut**. +The **fee** input parameter represents the swap fee that is deducted from the trade and given to liquidity providers. It is one of the identifiers of a Pool, the others being **tokenIn** and **tokenOut**. The guide will **cover**: -1. Computing the Pool's deployment address -2. Referencing the Pool contract and fetching metadata -3. Referencing the Quoter contract and getting a quote +1. Fetching a Quote for a simple swap on one Pool At the end of the guide, we should be able to fetch a quote for the given input token pair and the input token amount with the press of a button on the web application. @@ -37,6 +35,7 @@ We will use the example configuration `CurrentConfig` in most code snippets of t ```typescript import { Token } from '@uniswap/sdk-core' +import { FeeAmount } from '@uniswap/v3-sdk' interface ExampleConfig { rpc: { @@ -45,9 +44,9 @@ interface ExampleConfig { } tokens: { in: Token - amountIn: number + readableAmountIn: number out: Token - poolFee: number + poolFee: FeeAmount } } @@ -77,138 +76,57 @@ The pool used is defined by a pair of tokens in [`constants.ts`](https://github. You can also change these two tokens and the fee of the pool in the config, just make sure a Pool actually exists for your configuration. Check out the top pools on [Uniswap info](https://info.uniswap.org/#/pools). -## Computing the Pool's deployment address +Check out the full code for the following snippets in [quote.ts](https://github.com/Uniswap/examples/blob/main/v3-sdk/quoting/src/libs/quote.ts) -To interact with the **USDC - WETH** Pool contract, we first need to compute its deployment address. -If you haven't worked directly with smart contracts yet, check out this [guide](https://docs.alchemy.com/docs/smart-contract-basics) from Alchemy. -The SDK provides a utility method for that: +## Using the SwapQuoter class to fetch a quote -```typescript -import { computePoolAddress } from '@uniswap/v3-sdk' - -const currentPoolAddress = computePoolAddress({ - factoryAddress: POOL_FACTORY_CONTRACT_ADDRESS, - tokenA: CurrentConfig.tokens.in, - tokenB: CurrentConfig.tokens.out, - fee: CurrentConfig.tokens.poolFee, -}) -``` +To get quotes for trades, Uniswap has deployed a Quoter Contract. We will use this contract to fetch the output amount we can expect for our trade, without actually executing the trade. -Since each *Uniswap V3 Pool* is uniquely identified by 3 characteristics (token in, token out, fee), we use those -in combination with the address of the *PoolFactory* contract to compute the address of the **USDC - ETH** Pool. -These parameters have already been defined in our [constants.ts](https://github.com/Uniswap/examples/blob/main/v3-sdk/quoting/src/libs/constants.ts#L14) file: +The `SwapQuoter` class allows us to interact with the Quoter Contract. +We will use the `quoteExactInputSingle` function to fetch a quote for our swap: ```typescript -const WETH_TOKEN = new Token( - 1, - '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', - 18, - 'WETH', - 'Wrapped Ether' -) - -const USDC_TOKEN = new Token( - 1, - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - 6, - 'USDC', - 'USD//C' -) +async function quoteExactInputSingle( + amountIn: CurrencyAmount, + tokenOut: TOutput, + poolFee: FeeAmount, + provider: Provider +): Promise> ``` -These constants are used in the `config.ts` file, as mentioned in the Introduction. - -We can find the Pool Factory Contract address for our chain [here](../../../../contracts/v3/reference/Deployments.md). - -## Referencing the Pool contract and fetching metadata - -Now that we have the deployment address of the **USDC - ETH** Pool, we can construct an instance of an **ethers** `Contract` to interact with it: +The function expects a `CurrencyAmount` object. We use ethers to parse the input amount: ```typescript import { ethers } from 'ethers' +import { CurrencyAmount } from 'sdk-core' -const provider = new ethers.providers.JsonRpcProvider(rpcUrl) -const poolContract = new ethers.Contract( - currentPoolAddress, - IUniswapV3PoolABI.abi, - provider -) -``` - -To construct the *Contract* we need to provide the address of the contract, its ABI and the provider that will carry out the RPC call for us. -We get access to the contract's ABI through the [@uniswap/v3-core](https://www.npmjs.com/package/@uniswap/v3-core) package, which holds the core smart contracts of the Uniswap V3 protocol: - -```typescript -import IUniswapV3PoolABI from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json' -``` - -Having constructed our reference to the contract, we can now access its methods through our provider. -We use a batch `Promise` call. This approach queries state data concurrently, rather than sequentially, to minimize the chance of fetching out of sync data that may be returned if sequential queries are executed over the span of two blocks: - -```typescript -const [token0, token1, fee, liquidity, slot0] = await Promise.all([ - poolContract.token0(), - poolContract.token1(), - poolContract.fee(), - poolContract.liquidity(), - poolContract.slot0(), -]) -``` - -The return values of these methods will become inputs to the quote fetching function. -The `token0` and `token1` variables are the addresses of the tokens in the Pool and should not be mistaken for `Token` objects from the sdk. -For the full code, check out [`getPoolConstants()`](https://github.com/Uniswap/examples/blob/main/v3-sdk/quoting/src/libs/quote.ts#L35) in `quote.ts`. - -:::note -In this example, the metadata we fetch is already present in our inputs. This guide fetches this information first in order to show how to fetch any metadata, which will be expanded on in future guides. -::: - -## Referencing the Quoter contract and getting a quote - -To get quotes for trades, Uniswap has deployed a **Quoter Contract**. We will use this contract to fetch the output amount we can expect for our trade, without actually executing the trade. -Check out the full code for the following snippets in [quote.ts](https://github.com/Uniswap/examples/blob/main/v3-sdk/quoting/src/libs/quote.ts) - -Like we did for the Pool contract, we need to construct an instance of an **ethers** `Contract` for our Quoter contract in order to interact with it: +const rawInputAmount = ethers.utils.parseUnits( + CurrentConfig.tokens.amountIn, + CurrentConfig.tokens.in.decimals + ) -```typescript -const quoterContract = new ethers.Contract( - QUOTER_CONTRACT_ADDRESS, - Quoter.abi, - getProvider() +const currencyAmountIn = CurrencyAmount.fromRawAmount( + CurrentConfig.tokens.tokenIn, + rawInputAmount ) ``` -We get access to the contract's ABI through the [@uniswap/v3-periphery](https://www.npmjs.com/package/@uniswap/v3-periphery) package, which holds the periphery smart contracts of the Uniswap V3 protocol: +We can now use the SwapQuoter class to fetch a quote for our swap. We need a provider to connect to the blockchain: ```typescript -import Quoter from '@uniswap/v3-periphery/artifacts/contracts/lens/Quoter.sol/Quoter.json' -``` - -We get the QUOTE_CONTRACT_ADDRESS for our chain from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). - -We can now use our Quoter contract to obtain the quote. +import { SwapQuoter } from '@uniswap/v3-sdk' -In an ideal world, the quoter functions would be `view` functions, which would make them very easy to query on-chain with minimal gas costs. However, the Uniswap V3 Quoter contracts rely on state-changing calls designed to be reverted to return the desired data. This means calling the quoter will be very expensive and should not be called on-chain. +const provider = new ethers.providers.JsonRpcProvider(CurrentConfig.rpc.mainnet) -To get around this difficulty, we can use the `callStatic` method provided by the **ethers.js** `Contract` instances. -This is a useful method that submits a state-changing transaction to an Ethereum node, but asks the node to simulate the state change, rather than to execute it. Our script can then return the result of the simulated state change: - -```typescript -const quotedAmountOut = await quoterContract.callStatic.quoteExactInputSingle( - token0, - token1, - fee, - fromReadableAmount( - CurrentConfig.tokens.amountIn, - CurrentConfig.tokens.in.decimals - ).toString(), - 0 +const currencyAmountOut = await SwapQuoter.quoteExactInputSingle( + currencyAmountInt, + CurrentConfig.tokens.out, + CurrentConfig.tokens.poolFee, + provider ) ``` -The `fromReadableAmount()` function creates the amount of the smallest unit of a token from the full unit amount and the decimals. - -The result of the call is the number of output tokens you'd receive for the quoted swap. +The return value is a `CurrencyAmount` object of the expected output amount for our swap. It should be noted that `quoteExactInputSingle` is only 1 of 4 different methods that the quoter offers: diff --git a/docs/sdk/v3/guides/swaps/02-trading.md b/docs/sdk/v3/guides/swaps/02-trading.md index 868ed986db..f7e132c30f 100644 --- a/docs/sdk/v3/guides/swaps/02-trading.md +++ b/docs/sdk/v3/guides/swaps/02-trading.md @@ -108,7 +108,7 @@ If you cannot see the Tokens traded in your wallet, you possibly have to [import ## Constructing a route from pool information -To construct our trade, we will first create an model instance of a `Pool`. We create an **ethers** contract like in the [previous guide](./01-quoting.md#referencing-the-pool-contract-and-fetching-metadata). +To construct our trade, we will first create a model instance of a `Pool`. We create an **ethers** contract like in the [previous guide](./01-quoting.md#referencing-the-pool-contract-and-fetching-metadata). We will first extract the needed metadata from the relevant pool contract. Metadata includes both constant information about the pool as well as information about its current state stored in its first slot: ```typescript From 6a69a2dda14ea2dcb4c31e4a70312ff49c595893 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Fri, 10 Nov 2023 11:19:15 +0100 Subject: [PATCH 38/52] Update trading guide --- docs/sdk/v3/guides/swaps/01-quoting.md | 8 +- docs/sdk/v3/guides/swaps/02-trading.md | 186 ++++++----- .../v3/guides/swaps/03-simulate-offchain.md | 312 ++++++++++++++++++ .../swaps/{03-routing.md => 04-routing.md} | 0 4 files changed, 414 insertions(+), 92 deletions(-) create mode 100644 docs/sdk/v3/guides/swaps/03-simulate-offchain.md rename docs/sdk/v3/guides/swaps/{03-routing.md => 04-routing.md} (100%) diff --git a/docs/sdk/v3/guides/swaps/01-quoting.md b/docs/sdk/v3/guides/swaps/01-quoting.md index da67cde234..2b6a814b59 100644 --- a/docs/sdk/v3/guides/swaps/01-quoting.md +++ b/docs/sdk/v3/guides/swaps/01-quoting.md @@ -80,7 +80,7 @@ Check out the full code for the following snippets in [quote.ts](https://github. ## Using the SwapQuoter class to fetch a quote -To get quotes for trades, Uniswap has deployed a Quoter Contract. We will use this contract to fetch the output amount we can expect for our trade, without actually executing the trade. +To get quotes for trades, Uniswap has deployed a **Quoter Contract**. We will use this contract to fetch the output amount we can expect for our trade, without actually executing the trade. The `SwapQuoter` class allows us to interact with the Quoter Contract. We will use the `quoteExactInputSingle` function to fetch a quote for our swap: @@ -126,9 +126,11 @@ const currencyAmountOut = await SwapQuoter.quoteExactInputSingle( ) ``` +The function calls the Quoter contract and parses the response value. It only works on chains where Uniswap has officially deployed the QuoterV2 contract. + The return value is a `CurrencyAmount` object of the expected output amount for our swap. -It should be noted that `quoteExactInputSingle` is only 1 of 4 different methods that the quoter offers: +It should be noted that `quoteExactInputSingle` is only 1 of 4 different methods that the quoter **contract** offers: 1. `quoteExactInputSingle` - given the amount you want to swap, produces a quote for the amount out for a swap of a single pool 2. `quoteExactInput` - given the amount you want to swap, produces a quote for the amount out for a swap over multiple pools @@ -137,6 +139,8 @@ It should be noted that `quoteExactInputSingle` is only 1 of 4 different methods If we want to trade two tokens that do not share a pool with each other, we will need to make swaps over multiple pools. This is where the `quoteExactInput` and `quoteExactOutput` methods come in. + +In the `SwapQuoter` JS class, all 4 of these functions can be accessed with the `callQuoter` function, using a `Route` object. We will dive deeper into routing in the [routing guide](03-routing.md). For the `exactOutput` and `exactOutputSingle` methods, we need to keep in mind that a pool can not give us more than the amount of Tokens it holds. diff --git a/docs/sdk/v3/guides/swaps/02-trading.md b/docs/sdk/v3/guides/swaps/02-trading.md index f7e132c30f..63da6938ed 100644 --- a/docs/sdk/v3/guides/swaps/02-trading.md +++ b/docs/sdk/v3/guides/swaps/02-trading.md @@ -18,7 +18,7 @@ In this example we will trade between two ERC20 tokens: **WETH and USDC**. The t The guide will **cover**: 1. Constructing a route from pool information -2. Constructing an unchecked trade +2. Fetching a Quote for the route 3. Executing a trade At the end of the guide, we should be able to create and execute a trade between any two ERC20 tokens using the example's included UI. @@ -108,24 +108,52 @@ If you cannot see the Tokens traded in your wallet, you possibly have to [import ## Constructing a route from pool information -To construct our trade, we will first create a model instance of a `Pool`. We create an **ethers** contract like in the [previous guide](./01-quoting.md#referencing-the-pool-contract-and-fetching-metadata). -We will first extract the needed metadata from the relevant pool contract. Metadata includes both constant information about the pool as well as information about its current state stored in its first slot: +To construct our trade, we will first create a model instance of a `Pool`. We create an **ethers** provider like in the [previous guide](./01-quoting.md). +We will first extract the needed metadata from the relevant pool contract. Metadata includes both constant information about the pool as well as information about its current state stored in its first slot. +To interact with the Pool, we need to compute its contract address. We use the `computePoolAddress()` function to achieve that: ```typescript -async function getPoolInfo() { - const [token0, token1, fee, liquidity, slot0] = - await Promise.all([ - poolContract.fee(), - poolContract.liquidity(), - poolContract.slot0(), - ]) - - return { - fee, - liquidity, - sqrtPriceX96: slot0[0], - tick: slot0[1], - } +import { computePoolAddress } from '@uniswap/v3-sdk' + +const currentPoolAddress = computePoolAddress({ + factoryAddress: POOL_FACTORY_CONTRACT_ADDRESS, + tokenA: CurrentConfig.tokens.in, + tokenB: CurrentConfig.tokens.out, + fee: CurrentConfig.tokens.poolFee, +}) +``` + +Every Pool is uniquely identified by the two tokens it contains and its fee. +We can find the Pool Factory Contract address for our chain [here](https://docs.uniswap.org/contracts/v3/reference/deployments). + +We can now use the `getPoolData` function to fetch the metadata: + +```typescript +import { RPCPool } from '@uniswap/v3-sdk' +import { ethers } from 'ethers' + +const provider = new ethers.providers.JsonRpcProvider(CurrentConfig.rpc.mainnet) +const latestBlockNumber = await provider.getBlockNumber() + +const poolData = await RPCPool.getPoolData( + provider, + currentPoolAddress, + latestBlockNumber +) +``` + +The `getPoolData()` function returns a `PoolData` object: + +```typescript +interface PoolData { + address: string + tokenA: Token + tokenB: Token + fee: FeeAmount + sqrtPriceX96: BigInt + liquidity: BigInt + tick: number + tickSpacing: number } ``` @@ -143,15 +171,13 @@ You can find the full code in [`pool.ts`](https://github.com/Uniswap/examples/bl Using this metadata along with our inputs, we will then construct a `Pool`: ```typescript -const poolInfo = await getPoolInfo() - const pool = new Pool( CurrentConfig.tokens.in, CurrentConfig.tokens.out, - CurrentConfig.tokens.poolFee, - poolInfo.sqrtPriceX96.toString(), - poolInfo.liquidity.toString(), - poolInfo.tick + poolData.fee, + poolData.sqrtPriceX96, + poolData.liquidity, + poolData.tick ) ``` @@ -187,7 +213,6 @@ const swapRoute = new Route( Our `Route` understands that `CurrentConfig.tokens.in` should be traded for `CurrentConfig.tokens.out` over the Array of pools `[pool]`. - ## Constructing an unchecked trade Once we have constructed the route object, we now need to obtain a quote for the given `inputAmount` of the example: @@ -196,60 +221,44 @@ Once we have constructed the route object, we now need to obtain a quote for the const amountOut = await getOutputQuote(swapRoute) ``` -As shown below, the quote is obtained using the `v3-sdk`'s `SwapQuoter`, in contrast to the [previous quoting guide](./01-quoting.md), where we directly accessed the smart contact: +As shown below, the quote is obtained using the `v3-sdk`'s `SwapQuoter`, for this guide we use the `callQuoter()` function. +In contrast to the `quoteExactInputSingle()` function we used in the previous guide, this function works for any Route, not just a swap over a single Pool: ```typescript import { SwapQuoter } from '@uniswap/v3-sdk' import { CurrencyAmount, TradeType } from '@uniswap/sdk-core' -const { calldata } = await SwapQuoter.quoteCallParameters( - swapRoute, - CurrencyAmount.fromRawAmount( - CurrentConfig.tokens.in, - fromReadableAmount( - CurrentConfig.tokens.amountIn, - CurrentConfig.tokens.in.decimals +const rawInputAmount = ethers.utils.parseUnits( + CurrentConfig.tokens.amountIn, + CurrentConfig.tokens.in.decimals ) - ), + +const currencyAmountIn = CurrencyAmount.fromRawAmount( + CurrentConfig.tokens.tokenIn, + rawInputAmount +) + +const expectedOutput = await SwapQuoter.callQuoter( + swapRoute, + currencyAmountIn, TradeType.EXACT_INPUT, - { - useQuoterV2: true, - } + provider ) ``` -The `SwapQuoter`'s `quoteCallParameters` function, gives us the calldata needed to make the call to the `Quoter`, and we then decode the returned quote: - -```typescript -const quoteCallReturnData = await provider.call({ - to: QUOTER_CONTRACT_ADDRESS, - data: calldata, -}) - -return ethers.utils.defaultAbiCoder.decode(['uint256'], quoteCallReturnData) -``` +We construct the input the same way we did in the previous guide. +The return value of the `callQuoter()` function is the expected output, parsed as a `CurrencyAmount` object. With the quote and the route, we can now construct a trade using the route in addition to the output amount from a quote based on our input. Because we already know the expected output of our Trade, we do not have to check it again. We can use the `uncheckedTrade` function to create our Trade: ```typescript import { Trade } from 'uniswap/v3-sdk' -import { CurrencyAmount, TradeType } from '@uniswap/sdk-core' -import JSBI from 'jsbi' const uncheckedTrade = Trade.createUncheckedTrade({ route: swapRoute, - inputAmount: CurrencyAmount.fromRawAmount( - CurrentConfig.tokens.in, - fromReadableAmount( - CurrentConfig.tokens.amountIn, - CurrentConfig.tokens.in.decimals - ) - ), - outputAmount: CurrencyAmount.fromRawAmount( - CurrentConfig.tokens.out, - JSBI.BigInt(amountOut) - ), + inputAmount: currencyAmountIn, + outputAmount: expectedOutput, tradeType: TradeType.EXACT_INPUT, }) ``` @@ -258,55 +267,52 @@ This example uses an exact input trade, but we can also construct a trade using ## Executing a trade -Once we have created a trade, we can now execute this trade with our provider. First, we must give the `SwapRouter` approval to spend our tokens for us: - -```typescript -const tokenApproval = await getTokenTransferApproval(CurrentConfig.tokens.in) -``` - -You can find the approval function [here](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/src/libs/trading.ts#L151). -We will use this function or similar implementations in most guides. - -Then, we set our options that define how much time and slippage can occur in our execution as well as the address to use for our wallet: +Once we have created a trade, we can now execute this trade with our provider. +We will use the `executeTrade()` function of the `SwapRouter` class. +First we specify the deadline and the slippage tolerance we are willing to accept for our trade: ```typescript -import { SwapOptions } from '@uniswap/v3-sdk' +import { SwapOptions } frpm '@uniswap/v3-sdk' import { Percent } from '@uniswap/sdk-core' -const options: SwapOptions = { - slippageTolerance: new Percent(50, 10_000), // 50 bips, or 0.50% - deadline: Math.floor(Date.now() / 1000) + 60 * 20, // 20 minutes from the current Unix time - recipient: walletAddress, -} +const swapOptions: SwapOptions = { + slippageTolerance: new Percent(50, 10_000), + deadline: Math.floor(Date.now() / 1000) + 60 * 5, // 5 minutes from the current Unix time + recipient: walletAddress, + } ``` The slippage of our trade is the maximum decrease from our calculated output amount that we are willing to accept for this trade. -The deadline is the latest point in time when we want the transaction to go through. +The deadline is the latest point in time when we want the transaction to go through. If we set this value too high, the transaction could be left waiting for days and we would need to pay gas fees to cancel it. +The swapOptions are an optional parameter of the `executeTrade()` function and default to exactly what we specified here if they are not provided. -Next, we use the `SwapRouter` class, a representation of the Uniswap [SwapRouter Contract](https://github.com/Uniswap/v3-periphery/blob/v1.0.0/contracts/SwapRouter.sol), to get the associated call parameters for our trade and options: +As we want to execute a state changing transaction on the blockchain, we need a wallet to sign our transaction: ```typescript -import { SwapRouter } from '@uniswap/v3-sdk' +import { ethers } from 'ethers' -const methodParameters = SwapRouter.swapCallParameters([uncheckedTrade], options) +const wallet = getWallet() +wallet.connect(provider) ``` -Finally, we can construct a transaction from the method parameters and send the transaction: +We are now ready to execute our trade: ```typescript -const tx = { - data: methodParameters.calldata, - to: SWAP_ROUTER_ADDRESS, - value: methodParameters.value, - from: walletAddress, - maxFeePerGas: MAX_FEE_PER_GAS, - maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS, -} +import { SwapRouter } from '@uniswap/v3-sdk' -const res = await wallet.sendTransaction(tx) +const txResponse = await SwapRouter.executeTrade( + [uncheckedTrade], + swapOptions, + wallet +) ``` +The function automatically checks if the necessary token transfer approvals exist and creates them if not. +For this reason, we usually need to wait 2 blocks for the execution to finish. +The return value is an `ethers.TransactionResponse` object. + ## Next Steps -The resulting example allows for trading between any two ERC20 tokens, but this can be suboptimal for the best pricing and fees. To achieve the best possible price, we use the Uniswap auto router to route through pools to get an optimal cost. Our [routing](./03-routing.md) guide will show you how to use this router and execute optimal swaps. +So far, we have used onchain calls to get a quote for our trades. +In the next guide on [offchain simulations](03-simulate-offchain.md), we will use the sdk to fetch Pool information first and simulate our Trades offchain. diff --git a/docs/sdk/v3/guides/swaps/03-simulate-offchain.md b/docs/sdk/v3/guides/swaps/03-simulate-offchain.md new file mode 100644 index 0000000000..df49bdf051 --- /dev/null +++ b/docs/sdk/v3/guides/swaps/03-simulate-offchain.md @@ -0,0 +1,312 @@ +--- +id: simulate +title: Simulating trades offchain +--- + +## Introduction + +This guide will build off our [quoting guide](./01-quoting.md) and show how to use a quote to construct and execute a trade on the Uniswap V3 protocol. It is based on the [Trading code example](https://github.com/Uniswap/examples/tree/main/v3-sdk/trading), found in the Uniswap code examples [repository](https://github.com/Uniswap/examples). To run this example, check out the guide's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/README.md) and follow the setup instructions. + +:::info +If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](../01-background.md) page! + +To get started with local development, also check out the [local development guide](../02-local-development.md). +::: + +In this example we will trade between two ERC20 tokens: **WETH and USDC**. The tokens, amount of input token, and the fee level can be configured as inputs. + +The guide will **cover**: + +1. Constructing a route from pool information +2. Constructing an unchecked trade +3. Executing a trade + +At the end of the guide, we should be able to create and execute a trade between any two ERC20 tokens using the example's included UI. + +:::note +Included in the example application is functionality to wrap/unwrap ETH as needed to fund the example `WETH` to `USDC` swap directly from an `ETH` balance. +::: + +For this guide, the following Uniswap packages are used: + +- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) +- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) + +The core code of this guide can be found in [`trading.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/src/libs/trading.ts) + +## Using a wallet extension + +Like in the previous guide, our [example](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading) uses a [config file ](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/src/config.ts) to configurate the inputs used. +The strucuture is similar to the quoting config, but we also have the option to select an environment: + +```typescript +export interface ExampleConfig { + env: Environment + rpc: { + local: string + mainnet: string + } + wallet: { + address: string + privateKey: string + } + tokens: { + in: Token + amountIn: number + out: Token + poolFee: number + } +} +``` + +Per default, the env field is set to `Environment.LOCAL`: + +```typescript +export const CurrentConfig: ExampleConfig = { + env: Environment.LOCAL, + rpc: { + local: 'http://localhost:8545', + mainnet: '', + }, + wallet: { + address: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + privateKey: + '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', + }, + tokens: { + in: WETH_TOKEN, + amountIn: 1, + out: USDC_TOKEN, + poolFee: FeeAmount.MEDIUM, + }, +} +``` + +In this example, we have the option to use a Wallet Extension like Metamask to sign the transactions we are sending. To do so, let's change the Environment to `Environment.WALLET_EXTENSION`: + +```typescript +export const CurrentConfig: ExampleConfig = { + env: Environment.WALLET_EXTENSION, + rpc: { + local: 'http://localhost:8545', + }, + wallet: { + ... + }, + tokens: { + ... + }, +} +``` + +Run the example and then add the local network to your wallet browser extension, if you are using Metamask for example, follow [this guide](https://support.metamask.io/hc/en-us/articles/360043227612-How-to-add-a-custom-network-RPC). +You should also import a private key to use on your local network, for example `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80` from Foundry's example wallets. + +Consider checking out the [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/README.md) of the example. + +If you cannot see the Tokens traded in your wallet, you possibly have to [import them](https://support.metamask.io/hc/en-us/articles/360015489031-How-to-display-tokens-in-MetaMask). + +## Constructing a route from pool information + +To construct our trade, we will first create a model instance of a `Pool`. We create an **ethers** contract like in the [previous guide](./01-quoting.md#referencing-the-pool-contract-and-fetching-metadata). +We will first extract the needed metadata from the relevant pool contract. Metadata includes both constant information about the pool as well as information about its current state stored in its first slot: + +```typescript +async function getPoolInfo() { + const [token0, token1, fee, liquidity, slot0] = + await Promise.all([ + poolContract.fee(), + poolContract.liquidity(), + poolContract.slot0(), + ]) + + return { + fee, + liquidity, + sqrtPriceX96: slot0[0], + tick: slot0[1], + } +} +``` + +Before continuing, let's talk about the values we fetched here and what they represent: + +- `fee` is the fee that is taken from every swap that is executed on the pool in 1 per million - if the `fee` value of a pool is 500, ```500/ 1000000``` (or 0.05%) of the trade amount is taken as a fee. This fee goes to the liquidity providers of the Pool. +- `liquidity` is the amount of liquidity the Pool can use for trades at the current price. +- `sqrtPriceX96` is the current Price of the pool, encoded as a ratio between `token0` and `token1`. +- `tick` is the tick at the current price of the pool. + +Check out the [whitepaper](https://uniswap.org/whitepaper-v3.pdf) to learn more on how liquidity and ticks work in Uniswap V3. + +You can find the full code in [`pool.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/src/libs/pool.ts). + +Using this metadata along with our inputs, we will then construct a `Pool`: + +```typescript +const poolInfo = await getPoolInfo() + +const pool = new Pool( + CurrentConfig.tokens.in, + CurrentConfig.tokens.out, + CurrentConfig.tokens.poolFee, + poolInfo.sqrtPriceX96.toString(), + poolInfo.liquidity.toString(), + poolInfo.tick +) +``` + +## Creating a Route + +With this `Pool`, we can now construct a route to use in our trade. Routes represent a route over one or more pools from one Token to another. Let's imagine we have three pools: + +``` +- PoolA: USDC/ WETH +- PoolB: USDT/ WETH +- PoolC: USDT/ DAI +``` + +We would like to trade from USDC to DAI, so we create a route through our 3 pools: + +``` +PoolA -> PoolB -> PoolC +``` + +The `Route` object can find this route from an array of given pools and an input and output Token. + +To keep it simple for this guide, we only swap over one Pool: + +```typescript +import { Route } from '@uniswap/v3-sdk' + +const swapRoute = new Route( + [pool], + CurrentConfig.tokens.in, + CurrentConfig.tokens.out +) +``` + +Our `Route` understands that `CurrentConfig.tokens.in` should be traded for `CurrentConfig.tokens.out` over the Array of pools `[pool]`. + + +## Constructing an unchecked trade + +Once we have constructed the route object, we now need to obtain a quote for the given `inputAmount` of the example: + +```typescript +const amountOut = await getOutputQuote(swapRoute) +``` + +As shown below, the quote is obtained using the `v3-sdk`'s `SwapQuoter`, in contrast to the [previous quoting guide](./01-quoting.md), where we directly accessed the smart contact: + +```typescript +import { SwapQuoter } from '@uniswap/v3-sdk' +import { CurrencyAmount, TradeType } from '@uniswap/sdk-core' + +const { calldata } = await SwapQuoter.quoteCallParameters( + swapRoute, + CurrencyAmount.fromRawAmount( + CurrentConfig.tokens.in, + fromReadableAmount( + CurrentConfig.tokens.amountIn, + CurrentConfig.tokens.in.decimals + ) + ), + TradeType.EXACT_INPUT, + { + useQuoterV2: true, + } +) +``` + +The `SwapQuoter`'s `quoteCallParameters` function, gives us the calldata needed to make the call to the `Quoter`, and we then decode the returned quote: + +```typescript +const quoteCallReturnData = await provider.call({ + to: QUOTER_CONTRACT_ADDRESS, + data: calldata, +}) + +return ethers.utils.defaultAbiCoder.decode(['uint256'], quoteCallReturnData) +``` + +With the quote and the route, we can now construct a trade using the route in addition to the output amount from a quote based on our input. +Because we already know the expected output of our Trade, we do not have to check it again. We can use the `uncheckedTrade` function to create our Trade: + +```typescript +import { Trade } from 'uniswap/v3-sdk' +import { CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import JSBI from 'jsbi' + +const uncheckedTrade = Trade.createUncheckedTrade({ + route: swapRoute, + inputAmount: CurrencyAmount.fromRawAmount( + CurrentConfig.tokens.in, + fromReadableAmount( + CurrentConfig.tokens.amountIn, + CurrentConfig.tokens.in.decimals + ) + ), + outputAmount: CurrencyAmount.fromRawAmount( + CurrentConfig.tokens.out, + JSBI.BigInt(amountOut) + ), + tradeType: TradeType.EXACT_INPUT, +}) +``` + +This example uses an exact input trade, but we can also construct a trade using exact output assuming we adapt our quoting code accordingly. + +## Executing a trade + +Once we have created a trade, we can now execute this trade with our provider. First, we must give the `SwapRouter` approval to spend our tokens for us: + +```typescript +const tokenApproval = await getTokenTransferApproval(CurrentConfig.tokens.in) +``` + +You can find the approval function [here](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/src/libs/trading.ts#L151). +We will use this function or similar implementations in most guides. + +Then, we set our options that define how much time and slippage can occur in our execution as well as the address to use for our wallet: + +```typescript +import { SwapOptions } from '@uniswap/v3-sdk' +import { Percent } from '@uniswap/sdk-core' + +const options: SwapOptions = { + slippageTolerance: new Percent(50, 10_000), // 50 bips, or 0.50% + deadline: Math.floor(Date.now() / 1000) + 60 * 20, // 20 minutes from the current Unix time + recipient: walletAddress, +} +``` + +The slippage of our trade is the maximum decrease from our calculated output amount that we are willing to accept for this trade. +The deadline is the latest point in time when we want the transaction to go through. +If we set this value too high, the transaction could be left waiting for days and we would need to pay gas fees to cancel it. + +Next, we use the `SwapRouter` class, a representation of the Uniswap [SwapRouter Contract](https://github.com/Uniswap/v3-periphery/blob/v1.0.0/contracts/SwapRouter.sol), to get the associated call parameters for our trade and options: + +```typescript +import { SwapRouter } from '@uniswap/v3-sdk' + +const methodParameters = SwapRouter.swapCallParameters([uncheckedTrade], options) +``` + +Finally, we can construct a transaction from the method parameters and send the transaction: + +```typescript +const tx = { + data: methodParameters.calldata, + to: SWAP_ROUTER_ADDRESS, + value: methodParameters.value, + from: walletAddress, + maxFeePerGas: MAX_FEE_PER_GAS, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS, +} + +const res = await wallet.sendTransaction(tx) +``` + +## Next Steps + +The resulting example allows for trading between any two ERC20 tokens, but this can be suboptimal for the best pricing and fees. To achieve the best possible price, we use the Uniswap auto router to route through pools to get an optimal cost. Our [routing](./03-routing.md) guide will show you how to use this router and execute optimal swaps. diff --git a/docs/sdk/v3/guides/swaps/03-routing.md b/docs/sdk/v3/guides/swaps/04-routing.md similarity index 100% rename from docs/sdk/v3/guides/swaps/03-routing.md rename to docs/sdk/v3/guides/swaps/04-routing.md From dd29db309322bbab0af201d41059eaa9bcc1371d Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Fri, 10 Nov 2023 12:58:02 +0100 Subject: [PATCH 39/52] Add simulate offchain trades guide and update routing guide --- .../v3/guides/swaps/03-simulate-offchain.md | 313 +++++------------- docs/sdk/v3/guides/swaps/04-routing.md | 73 +--- 2 files changed, 109 insertions(+), 277 deletions(-) diff --git a/docs/sdk/v3/guides/swaps/03-simulate-offchain.md b/docs/sdk/v3/guides/swaps/03-simulate-offchain.md index df49bdf051..6bab605cd2 100644 --- a/docs/sdk/v3/guides/swaps/03-simulate-offchain.md +++ b/docs/sdk/v3/guides/swaps/03-simulate-offchain.md @@ -1,11 +1,11 @@ --- id: simulate -title: Simulating trades offchain +title: Simulating Trades --- ## Introduction -This guide will build off our [quoting guide](./01-quoting.md) and show how to use a quote to construct and execute a trade on the Uniswap V3 protocol. It is based on the [Trading code example](https://github.com/Uniswap/examples/tree/main/v3-sdk/trading), found in the Uniswap code examples [repository](https://github.com/Uniswap/examples). To run this example, check out the guide's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/README.md) and follow the setup instructions. +In this guide, we will fetch tickdata for several pools and simulate trades offchain to find the best possible route, before executing our trade onchain. :::info If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](../01-background.md) page! @@ -13,300 +13,169 @@ If you need a briefer on the SDK and to learn more about how these guides connec To get started with local development, also check out the [local development guide](../02-local-development.md). ::: -In this example we will trade between two ERC20 tokens: **WETH and USDC**. The tokens, amount of input token, and the fee level can be configured as inputs. +In this example we will trade between two ERC20 tokens: **USDC and DAI**. The tokens, amount of input token, and the available Pools can be configured as inputs. The guide will **cover**: -1. Constructing a route from pool information -2. Constructing an unchecked trade -3. Executing a trade +1. Constructing a fully initialized Pool +2. Simulating Swaps +3. Executing a Trade -At the end of the guide, we should be able to create and execute a trade between any two ERC20 tokens using the example's included UI. - -:::note -Included in the example application is functionality to wrap/unwrap ETH as needed to fund the example `WETH` to `USDC` swap directly from an `ETH` balance. -::: +At the end of the guide, we should be able to initialize Pools with full tickdata and simulate trades offchain. For this guide, the following Uniswap packages are used: - [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) - [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) -The core code of this guide can be found in [`trading.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/src/libs/trading.ts) +The core code of this guide can be found in [`simulations.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/offchain-simulation/src/libs/simulations.ts) -## Using a wallet extension +## Example configuration -Like in the previous guide, our [example](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading) uses a [config file ](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/src/config.ts) to configurate the inputs used. -The strucuture is similar to the quoting config, but we also have the option to select an environment: +We will use the example configuration `CurrentConfig` in most code snippets of this guide. It has the format: ```typescript -export interface ExampleConfig { - env: Environment +import { Token } from '@uniswap/sdk-core' +import { FeeAmount, Pool } from '@uniswap/v3-sdk' + +interface ExampleConfig { rpc: { - local: string + local: string, mainnet: string } - wallet: { - address: string - privateKey: string - } tokens: { in: Token - amountIn: number + readableAmountIn: number out: Token - poolFee: number - } -} -``` - -Per default, the env field is set to `Environment.LOCAL`: - -```typescript -export const CurrentConfig: ExampleConfig = { - env: Environment.LOCAL, - rpc: { - local: 'http://localhost:8545', - mainnet: '', - }, - wallet: { - address: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', - privateKey: - '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', - }, - tokens: { - in: WETH_TOKEN, - amountIn: 1, - out: USDC_TOKEN, - poolFee: FeeAmount.MEDIUM, }, + pools: Pool[] } + +export const CurrentConfig: ExampleConfig = {...} ``` -In this example, we have the option to use a Wallet Extension like Metamask to sign the transactions we are sending. To do so, let's change the Environment to `Environment.WALLET_EXTENSION`: +The default config of the example uses a local fork of mainnet. If you haven't already, check out our [local development guide](../02-local-development.md). +To change the rpc endpoint or the Pools used, edit the [`Currentconfig`](https://github.com/Uniswap/examples/blob/main/v3-sdk/quoting/src/config.ts#L21). ```typescript export const CurrentConfig: ExampleConfig = { - env: Environment.WALLET_EXTENSION, rpc: { local: 'http://localhost:8545', - }, - wallet: { - ... + mainnet: '...' }, tokens: { - ... + in: USDC_TOKEN, + readableAmountIn: 1000, + out: DAI_TOKEN, }, + pools: [ + USDC_WETH_Pool, + USDT_WETH_Pool, + USDT_DAI_Pool + ] } ``` -Run the example and then add the local network to your wallet browser extension, if you are using Metamask for example, follow [this guide](https://support.metamask.io/hc/en-us/articles/360043227612-How-to-add-a-custom-network-RPC). -You should also import a private key to use on your local network, for example `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80` from Foundry's example wallets. +Fetching the TickData for a Pool requires a lot of RPC requests. Make sure you are aware of the costs of your RPC provider and strongly consider using a websocket provider for performance. +Free, public rpc endpoints are usually not fast enough to fetch this amount of data in a sensible time. +This functionality of the sdk will usually be used in a backend. -Consider checking out the [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/README.md) of the example. +The pools used in the configuration are imported from `constants.ts`. You can add any Pool you want to the array, just make sure they exist onchain. +Check out the top pools on [Uniswap info](https://info.uniswap.org/#/pools). -If you cannot see the Tokens traded in your wallet, you possibly have to [import them](https://support.metamask.io/hc/en-us/articles/360015489031-How-to-display-tokens-in-MetaMask). +## Initializing the Pools -## Constructing a route from pool information +We already initialized a Pool object in the previous guide using the `getPoolData` function. +By default, a Pool does not hold the full Tickdata necessary to compute trades, as fetching it is not necessary for most use cases and can be expensive. -To construct our trade, we will first create a model instance of a `Pool`. We create an **ethers** contract like in the [previous guide](./01-quoting.md#referencing-the-pool-contract-and-fetching-metadata). -We will first extract the needed metadata from the relevant pool contract. Metadata includes both constant information about the pool as well as information about its current state stored in its first slot: +To fetch the full Tickdata for the Pool, we use the `initializeTicks()` function on our Pools: ```typescript -async function getPoolInfo() { - const [token0, token1, fee, liquidity, slot0] = - await Promise.all([ - poolContract.fee(), - poolContract.liquidity(), - poolContract.slot0(), - ]) - - return { - fee, - liquidity, - sqrtPriceX96: slot0[0], - tick: slot0[1], - } -} -``` - -Before continuing, let's talk about the values we fetched here and what they represent: - -- `fee` is the fee that is taken from every swap that is executed on the pool in 1 per million - if the `fee` value of a pool is 500, ```500/ 1000000``` (or 0.05%) of the trade amount is taken as a fee. This fee goes to the liquidity providers of the Pool. -- `liquidity` is the amount of liquidity the Pool can use for trades at the current price. -- `sqrtPriceX96` is the current Price of the pool, encoded as a ratio between `token0` and `token1`. -- `tick` is the tick at the current price of the pool. - -Check out the [whitepaper](https://uniswap.org/whitepaper-v3.pdf) to learn more on how liquidity and ticks work in Uniswap V3. - -You can find the full code in [`pool.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/src/libs/pool.ts). - -Using this metadata along with our inputs, we will then construct a `Pool`: - -```typescript -const poolInfo = await getPoolInfo() - -const pool = new Pool( - CurrentConfig.tokens.in, - CurrentConfig.tokens.out, - CurrentConfig.tokens.poolFee, - poolInfo.sqrtPriceX96.toString(), - poolInfo.liquidity.toString(), - poolInfo.tick -) -``` +import { ethers } from 'ethers' -## Creating a Route +const provider = new ethers.providers.JsonRpcProvider(CurrenConfig.rpc.local) -With this `Pool`, we can now construct a route to use in our trade. Routes represent a route over one or more pools from one Token to another. Let's imagine we have three pools: +let promises = [] -``` -- PoolA: USDC/ WETH -- PoolB: USDT/ WETH -- PoolC: USDT/ DAI -``` - -We would like to trade from USDC to DAI, so we create a route through our 3 pools: - -``` -PoolA -> PoolB -> PoolC -``` - -The `Route` object can find this route from an array of given pools and an input and output Token. - -To keep it simple for this guide, we only swap over one Pool: - -```typescript -import { Route } from '@uniswap/v3-sdk' +let pools = CurrentConfig.pools +for (let pool of pools) { + const request = pool.initializeTicks(provider) + promises.push(request) +} -const swapRoute = new Route( - [pool], - CurrentConfig.tokens.in, - CurrentConfig.tokens.out -) +await Promise.all(promises) ``` -Our `Route` understands that `CurrentConfig.tokens.in` should be traded for `CurrentConfig.tokens.out` over the Array of pools `[pool]`. - +Our pools are now fully initialized and we can use them to simulate our trade offchain. -## Constructing an unchecked trade +## Simulating trades -Once we have constructed the route object, we now need to obtain a quote for the given `inputAmount` of the example: +In this example, we want to make an exact Input trade from **USDC** to **DAI**. +We use the `Trade.bestTradeExactIn` function to find the best route given our Pools: ```typescript -const amountOut = await getOutputQuote(swapRoute) -``` +import { Trade, BestTradeOptions } from '@uniswap/v3-sdk' +import { CurrencyAmount } from '@uniswap/sdk-core' -As shown below, the quote is obtained using the `v3-sdk`'s `SwapQuoter`, in contrast to the [previous quoting guide](./01-quoting.md), where we directly accessed the smart contact: +const currencyAmountIn = CurrencyAmount.fromRawAmount(...) -```typescript -import { SwapQuoter } from '@uniswap/v3-sdk' -import { CurrencyAmount, TradeType } from '@uniswap/sdk-core' - -const { calldata } = await SwapQuoter.quoteCallParameters( - swapRoute, - CurrencyAmount.fromRawAmount( - CurrentConfig.tokens.in, - fromReadableAmount( - CurrentConfig.tokens.amountIn, - CurrentConfig.tokens.in.decimals - ) - ), - TradeType.EXACT_INPUT, - { - useQuoterV2: true, - } +const bestTrades = Trade.bestTradeExactIn( + pools, + currencyAmountIn, + CurrentConfig.tokens.out ) -``` - -The `SwapQuoter`'s `quoteCallParameters` function, gives us the calldata needed to make the call to the `Quoter`, and we then decode the returned quote: - -```typescript -const quoteCallReturnData = await provider.call({ - to: QUOTER_CONTRACT_ADDRESS, - data: calldata, -}) -return ethers.utils.defaultAbiCoder.decode(['uint256'], quoteCallReturnData) +const bestTrade = bestTrades[0] ``` -With the quote and the route, we can now construct a trade using the route in addition to the output amount from a quote based on our input. -Because we already know the expected output of our Trade, we do not have to check it again. We can use the `uncheckedTrade` function to create our Trade: +The function allows us to optionally define `BestTradeOptions`. +The maxNumResults is the maximum number of Trades that should be returned, the maxHops is the maximum number of Pools that should be used in a single Route in one of those trades. +We will keep the default configuration in this example: ```typescript -import { Trade } from 'uniswap/v3-sdk' -import { CurrencyAmount, TradeType } from '@uniswap/sdk-core' -import JSBI from 'jsbi' - -const uncheckedTrade = Trade.createUncheckedTrade({ - route: swapRoute, - inputAmount: CurrencyAmount.fromRawAmount( - CurrentConfig.tokens.in, - fromReadableAmount( - CurrentConfig.tokens.amountIn, - CurrentConfig.tokens.in.decimals - ) - ), - outputAmount: CurrencyAmount.fromRawAmount( - CurrentConfig.tokens.out, - JSBI.BigInt(amountOut) - ), - tradeType: TradeType.EXACT_INPUT, -}) +{ maxNumResults = 3, maxHops = 3 } ``` -This example uses an exact input trade, but we can also construct a trade using exact output assuming we adapt our quoting code accordingly. +We can get the output amount and the routes that are used by the Trade from the object returned. +The `bestTradeExactIn` function considers splitting the trade between multiple routes, so for larger trades the input amount may be split between several routes. +We can access them with the `swaps` getter if we want to access this information. -## Executing a trade +## Executing the trade -Once we have created a trade, we can now execute this trade with our provider. First, we must give the `SwapRouter` approval to spend our tokens for us: +Like in the previous guide, we execute the trade with `executeTrade()` on the `SwapRouter` class: ```typescript -const tokenApproval = await getTokenTransferApproval(CurrentConfig.tokens.in) -``` - -You can find the approval function [here](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/src/libs/trading.ts#L151). -We will use this function or similar implementations in most guides. - -Then, we set our options that define how much time and slippage can occur in our execution as well as the address to use for our wallet: - -```typescript -import { SwapOptions } from '@uniswap/v3-sdk' -import { Percent } from '@uniswap/sdk-core' +import { SwapRouter } from '@uniswap/v3-sdk' -const options: SwapOptions = { - slippageTolerance: new Percent(50, 10_000), // 50 bips, or 0.50% - deadline: Math.floor(Date.now() / 1000) + 60 * 20, // 20 minutes from the current Unix time - recipient: walletAddress, -} +wallet.connect(provider) +const txResponse = SwapRouter.executeTrade( + [bestTrade[0]], + , + wallet +) ``` -The slippage of our trade is the maximum decrease from our calculated output amount that we are willing to accept for this trade. -The deadline is the latest point in time when we want the transaction to go through. -If we set this value too high, the transaction could be left waiting for days and we would need to pay gas fees to cancel it. +### Doing everything at once -Next, we use the `SwapRouter` class, a representation of the Uniswap [SwapRouter Contract](https://github.com/Uniswap/v3-periphery/blob/v1.0.0/contracts/SwapRouter.sol), to get the associated call parameters for our trade and options: +If you are not interested in the quote or don't want to initialize the pools yourself, you can directly execute a swap by using the `executeBestSimulatedSwapOnPools()` function: ```typescript -import { SwapRouter } from '@uniswap/v3-sdk' +wallet.connect(provider) -const methodParameters = SwapRouter.swapCallParameters([uncheckedTrade], options) +const txResponse = SwapRouter.executeBestSimulatedSwapOnPools( + pools, + currencyAmountIn, + CurrentConfig.tokens.in, + CurrentConfig.tokens.out, + TradeType.EXACT_INPUT, + undefined, + undefined, + wallet +) ``` -Finally, we can construct a transaction from the method parameters and send the transaction: - -```typescript -const tx = { - data: methodParameters.calldata, - to: SWAP_ROUTER_ADDRESS, - value: methodParameters.value, - from: walletAddress, - maxFeePerGas: MAX_FEE_PER_GAS, - maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS, -} - -const res = await wallet.sendTransaction(tx) -``` +This function is an abstraction of all the steps laid out in this guide and directly executes the best trade found by the `bestTradeExactIn` or `bestTradeExactOut` functions. ## Next Steps -The resulting example allows for trading between any two ERC20 tokens, but this can be suboptimal for the best pricing and fees. To achieve the best possible price, we use the Uniswap auto router to route through pools to get an optimal cost. Our [routing](./03-routing.md) guide will show you how to use this router and execute optimal swaps. +Now that you are familiar with simulating Trades and finding optimal routes offchain, we will demonstrate an alternative solution for routing in the [next guide](04-routing.md). diff --git a/docs/sdk/v3/guides/swaps/04-routing.md b/docs/sdk/v3/guides/swaps/04-routing.md index 2e35b64590..1c4521c2cd 100644 --- a/docs/sdk/v3/guides/swaps/04-routing.md +++ b/docs/sdk/v3/guides/swaps/04-routing.md @@ -72,7 +72,7 @@ const router = new AlphaRouter({ ## Creating a route We will use the [SwapRouter02](https://github.com/Uniswap/v3-periphery/blob/v1.0.0/contracts/SwapRouter.sol) for our trade. -The `smart-order-router` package provides us with a SwapOptionsSwapRouter02` interface, defining the wallet to use, slippage tolerance, and deadline for the transaction that we need to interact with the contract: +The `smart-order-router` package provides us with a `SwapOptionsSwapRouter02` interface, defining the wallet to use, slippage tolerance, and deadline for the transaction that we need to interact with the contract: ```typescript import { SwapOptionsSwapRouter02, SwapType } from '@uniswap/smart-order-router' @@ -86,7 +86,7 @@ const options: SwapOptionsSwapRouter02 = { } ``` -Like explained in the [previous guide](./02-trading.md#executing-a-trade), it is important to set the parameters to sensible values. +Like explained in the [trading guide](./02-trading.md#executing-a-trade), it is important to set the parameters to sensible values. Using these options, we can now create a trade (`TradeType.EXACT_INPUT` or `TradeType.EXACT_OUTPUT`) with the currency and the input amount to use to get a quote. For this example, we'll use an `EXACT_INPUT` trade to get a quote outputted in the quote currency. @@ -97,34 +97,19 @@ const rawTokenAmountIn: JSBI = fromReadableAmount( CurrentConfig.currencies.amountIn, CurrentConfig.currencies.in.decimals ) - -const route = await router.route( - CurrencyAmount.fromRawAmount( + const currencyAmountIn = CurrencyAmount.fromRawAmount( CurrentConfig.currencies.in, rawTokenAmountIn - ), + ) + +const route = await router.route( + currencyAmountIn, CurrentConfig.currencies.out, TradeType.EXACT_INPUT, options ) ``` -The `fromReadableAmount` function calculates the amount of tokens in the Token's smallest unit from the full unit and the Token's decimals: - -```typescript title="src/libs/conversion.ts" -export function fromReadableAmount(amount: number, decimals: number): JSBI { - const extraDigits = Math.pow(10, countDecimals(amount)) - const adjustedAmount = amount * extraDigits - return JSBI.divide( - JSBI.multiply( - JSBI.BigInt(adjustedAmount), - JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(decimals)) - ), - JSBI.BigInt(extraDigits) - ) -} -``` - `route` and `route.methodParameters` are *optional* as the request can fail, for example if **no route exists** between the two Tokens or because of networking issues. We check if the call was succesful: @@ -138,44 +123,22 @@ Depending on our preferences and reason for the issue we could retry the request ## Swapping using a route -First, we need to give approval to the `SwapRouter` smart contract to spend our tokens for us: - -```typescript -import { ethers } from 'ethers' -... - -const wallet = new ethers.Wallet(privateKey, provider) -const tokenContract = new ethers.Contract( - CurrentConfig.tokens.in.address, - ERC20ABI, - wallet -) -const tokenApproval = await tokenContract.approve( - V3_SWAP_ROUTER_ADDRESS, - ethers.BigNumber.from(rawTokenAmountIn.toString()) -) -``` - To be able to spend the tokens of a wallet, a smart contract first needs to get an approval from that wallet. ERC20 tokens have an `approve` function that accepts the address of the smart contract that we want to allow spending our tokens and the amount the smart contract should be allowed to spend. +The `executeQuotedSwapFromRoute()` function takes care of this, but possibly needs two blocks for the execution for this reason. -We can get the **V3_SWAP_ROUTER_ADDRESS** for our chain from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). -Keep in mind that different chains might have **different deployment addresses** for the same contracts. -The deployment address for local forks of a network are the same as in the network you forked, so for a **fork of mainnet** it would be the address for **Mainnet**. - -We need to wait one block for the approval transaction to be included by the blockchain. +```typescript +import { SwapRouter, TradeType } from '@uniswap/v3-sdk' -Once the approval has been granted, we can now execute the trade using the route's computed calldata, values, and gas values: +wallet.connect(provider) -```typescript -const txRes = await wallet.sendTransaction({ - data: route.methodParameters.calldata, - to: V3_SWAP_ROUTER_ADDRESS, - value: route.methodParameters.value, - from: wallet.address, - maxFeePerGas: MAX_FEE_PER_GAS, - maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS, -}) +const txResponse = await executeQuotedSwapFromRoute( + route, + currencyAmountIn, + TradeType.EXACT_INPUT, + undefined, + wallet +) ``` After swapping, you should see the currency balances update in the UI shortly after the block is confirmed. From 3334cfb21b83d18a29662da000eb5385445856fa Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Tue, 28 Nov 2023 13:34:42 +0100 Subject: [PATCH 40/52] Remove deprecated pool data guide --- ...ve-liquidity.md => 02-active-liquidity.md} | 0 docs/sdk/v3/guides/advanced/02-pool-data.md | 369 ------------------ ...{04-price-oracle.md => 03-price-oracle.md} | 0 ...{05-range-orders.md => 04-range-orders.md} | 3 +- docs/sdk/v3/guides/swaps/04-routing.md | 4 +- 5 files changed, 3 insertions(+), 373 deletions(-) rename docs/sdk/v3/guides/advanced/{03-active-liquidity.md => 02-active-liquidity.md} (100%) delete mode 100644 docs/sdk/v3/guides/advanced/02-pool-data.md rename docs/sdk/v3/guides/advanced/{04-price-oracle.md => 03-price-oracle.md} (100%) rename docs/sdk/v3/guides/advanced/{05-range-orders.md => 04-range-orders.md} (99%) diff --git a/docs/sdk/v3/guides/advanced/03-active-liquidity.md b/docs/sdk/v3/guides/advanced/02-active-liquidity.md similarity index 100% rename from docs/sdk/v3/guides/advanced/03-active-liquidity.md rename to docs/sdk/v3/guides/advanced/02-active-liquidity.md diff --git a/docs/sdk/v3/guides/advanced/02-pool-data.md b/docs/sdk/v3/guides/advanced/02-pool-data.md deleted file mode 100644 index cbcdd0e260..0000000000 --- a/docs/sdk/v3/guides/advanced/02-pool-data.md +++ /dev/null @@ -1,369 +0,0 @@ ---- -id: pool-data -title: Fetching Pool Data ---- - -## Introduction - -This guide will cover how to initialize a Pool with full tick data to allow offchain calculations. It is based on the [Fetching Pool data example](https://github.com/Uniswap/examples/tree/main/v3-sdk/pool-data), found in the Uniswap code examples [repository](https://github.com/Uniswap/examples). To run this example, check out the guide's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/pool-data/README.md) and follow the setup instructions. - -:::info -If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](./01-background.md) page! -::: - -In this example we will use **ethers JS** and **ethers-multicall** to construct a `Pool` object that we can use in the following guides. - -This guide will **cover**: - -1. Computing the Pool's address -2. Referencing the Pool contract and fetching metadata -3. Fetching the positions of all initialized Ticks with multicall -4. Fetching all ticks by their indices with a multicall -5. Constructing the Pool object - -At the end of the guide, we will have created a `Pool` Object that accurately represents the state of a V3 pool at the time we fetched it. - -For this guide, the following Uniswap packages are used: - -- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) -- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) - -We will also use the `ethers-multicall` npm package: - -- [`ethers-multicall`](https://www.npmjs.com/package/ethers-multicall) - -The core code of this guide can be found in [`fetcher.ts`](https://github.com/Uniswap/examples/tree/main/v3-sdk/multicall/src/libs/fetcher.ts) - -## Configuration - -The example accompanying this guide can be configured in the [`config.ts`](https://github.com/Uniswap/examples/tree/main/v3-sdk/multicall/src/config.ts) file. -The default configuration defines the rpc endpoint and the pool that is used for this guide: - -```typescript -export const CurrentConfig: ExampleConfig = { - env: Environment.MAINNET, - rpc: { - local: 'http://localhost:8545', - mainnet: 'https://mainnet.infura.io/v3/0ac57a06f2994538829c14745750d721', - }, - ... - pool: { - token0: USDC_TOKEN, - token1: WETH_TOKEN, - fee: FeeAmount.MEDIUM, - }, -} -``` - -FeeAmount.MEDIUM means that the pool has a swap fee of **0.3%**. -The `USDC_TOKEN` and `WETH_TOKEN` are defined in the [`constants.ts`](https://github.com/Uniswap/examples/tree/main/v3-sdk/multicall/src/libs/constants.ts) file: - -```typescript -export const WETH_TOKEN = new Token( - SupportedChainId.MAINNET, - '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', - 18, - 'WETH', - 'Wrapped Ether' -) - -export const USDC_TOKEN = new Token( - SupportedChainId.MAINNET, - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - 6, - 'USDC', - 'USD//C' -) -``` - -## Computing the Pool's deployment address - -In this example, we will construct the **USDC - WETH** Pool with **MEDIUM** fees. The SDK provides a method to compute the address: - -```typescript -import { Pool } from '@uniswap/v3-sdk' -import { CurrentConfig } from '../config.ts' - -const poolAddress = Pool.getAddress( - CurrentConfig.pool.token0, - CurrentConfig.pool.token1, - CurrentConfig.pool.fee - ) -``` - -Uniswap V3 allows 4 different Fee tiers when deploying a pool, so multiple pools can exist for each pair of tokens. - -## Creating a Pool Contract instance and fetching metadata - -Now that we have the address of a **USDC - ETH** Pool, we can construct an instance of an **ethers** `Contract` to interact with it. -To construct the Contract we need to provide the address of the contract, its ABI and a provider connected to an [RPC endpoint](https://www.chainnodes.org/docs). We get access to the contract's ABI through the `@uniswap/v3-core` package, which holds the core smart contracts of the Uniswap V3 protocol: - -```typescript -import { ethers } from 'ethers -import IUniswapV3PoolABI from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json' - -const provider = getProvider() -const poolContract = new ethers.Contract( - poolAddress, - IUniswapV3PoolABI.abi, - provider -) -``` - -The `getProvider()` function returns an `ethers.providers.JsonRpcProvider` with either the local or mainnet rpc url that we defined, depending on the Environment that we set in `config.ts`. - -Once we have set up our reference to the contract, we can proceed to access its methods. To construct our offchain representation of the Pool Contract, we need to fetch its liquidity, sqrtPrice, currently active tick and the full Tick data. -We get the **liquidity**, **sqrtPrice** and **tick** directly from the blockchain by calling `liquidity()`and `slot0()` on the Pool contract: - -```typescript -const [liquidity, slot0] = - await Promise.all([ - poolContract.liquidity(), - poolContract.slot0(), - ]) -``` - -The [slot0 function](../../../../contracts/v3/reference/core/interfaces/pool/IUniswapV3PoolState.md#slot0) represents the first (0th) storage slot of the pool and exposes multiple useful values in a single function: - -```solidity - function slot0( - ) external view returns ( - uint160 sqrtPriceX96, - int24 tick, - uint16 observationIndex, - uint16 observationCardinality, - uint16 observationCardinalityNext, - uint8 feeProtocol, - bool unlocked - ) -``` - -For our use case, we only need the `sqrtPriceX96` and the currently active `tick`. - -## Fetching all Ticks - -V3 pools use ticks to [concentrate liquidity](../../../../concepts/protocol/concentrated-liquidity.md) in price ranges and allow for better pricing of trades. -Even though most Pools only have a couple of **initialized ticks**, it is possible that a pools liquidity is defined by thousands of **initialized ticks**. -In that case, it can be very expensive or slow to get all of them with normal RPC calls. - -If you are not familiar with the concept of ticks, check out the [`introduction`](./01-introduction.md). - -To access tick data, we will use the `ticks` function of the V3 Pool contract: - -```solidity - function ticks( - int24 tick - ) external view returns ( - uint128 liquidityGross, - int128 liquidityNet, - uint256 feeGrowthOutside0X128, - uint256 feeGrowthOutside1X128, - int56 tickCumulativeOutside, - uint160 secondsPerLiquidityOutsideX128, - uint32 secondsOutside, - bool initialized - ) -``` - -The `tick` parameter that we provide the function with is the **index** (memory position) of the Tick we are trying to fetch. -To get the indices of all initialized Ticks of the Pool, we can calculate them from the **tickBitmaps**. -To fetch a `tickBitmap` function of the V3 Pool: - -```solidity - function tickBitmap( - int16 wordPosition - ) external view returns (uint256) -``` - -A pool stores lots of bitmaps, each of which contain the status of 256 Ticks. -The parameter `int16 wordPosition` the function accepts is the position of the bitMap we want to fetch. -We can calculate all the position of bitMaps (or words as they are sometimes called) from the `tickSpacing` of the Pool, which is in turn dependant on the Fee tier. - -So to summarise we need 4 steps to fetch all initialized ticks: - -1. Calculate all bitMap positions from the tickSpacing of the Pool. -2. Fetch all bitMaps using their positions. -3. Calculate the memory positions of all Ticks from the bitMaps. -4. Fetch all Ticks by their memory position. - -We will use multicalls for the fetch calls. - -## Multicall - -Multicall contracts **aggregate results** from multiple contract calls and therefore allow sending multiple contract calls in **one RPC request**. -This can improve the **speed** of fetching large amounts of data significantly and ensures that the data fetched is all from the **same block**. - -We will use the Multicall2 contract by MakerDAO. -We use the `ethers-muticall` npm package to easily interact with the Contract. - -## Calculating all bitMap positions - -As mentioned, Uniswap V3 Pools store **bitmaps**, also called *words*, that represent the state of **256 initializable ticks** at a time. -The value at a bit of a word is 1 if the tick at this index is initialized and 0 if it isn't. -We can calculate the positions of initialized ticks from the **words** of the Pool. - -All ticks of Uniswap V3 pools are between the indices `-887272` and `887272`. -We can calculate the minimum and maximum word from these indices and the Pool's tickSpacing: - -```typescript -function tickToWord(tick: number): number { - let compressed = Math.floor(tick / tickSpacing) - if (tick < 0 && tick % tickSpacing !== 0) { - compressed -= 1 - } - return tick >> 8 -} - -const minWord = tickToWord(-887272) -const maxWord = tickToWord(887272) -``` - -Ticks can only be initialized at indices that are **divisible by the tickSpacing**. -One word contains 256 ticks, so we can compress the ticks by right shifting 8 bit. - -## Fetching bitMaps from their position - -Knowing the positions of words in the Pool contract, we can now fetch them from the Pool using multicall and the `tickBitmap` read call. - -First we initialize our multicall providers and Pool Contract: - -```typescript -import { ethers } from 'ethers' -import { Contract, Provider } from 'ethers-multicall' - -const ethersProvider = new ethers.providers.JsonRpcProvider("...rpcUrl") -const multicallProvider = new Provider(ethersProvider) -await multicallProvider.init() - -const poolContract = new Contract(poolAddress, IUniswapV3PoolABI.abi) -``` - -The `multicallProvider` creates the multicall request and sends it via the ethers Provider. - -Next we loop through all possible word positions and add a `tickBitmap` call for each: - -```typescript -let calls: any[] = [] -let wordPosIndices: number[] = [] -for (let i = minWord; i <= maxWord; i++) { - wordPosIndices.push(i) - calls.push(poolContract.tickBitmap(i)) -} -``` - -We also keep track of the word position indices to be able to loop through them in the same order we added the calls to the array. - -We use the `multicallProvider.all()` function to send a multicall and map the results: - -```typescript -const results: bigint[] = (await multicallProvider.all(calls)).map( - (ethersResponse) => { - return BigInt(ethersResponse.toString()) - } - ) -``` - -A great visualization of what the bitMaps look like can be found in the [Uniswap V3 development book](https://uniswapv3book.com/docs/milestone_2/tick-bitmap-index/): - -TickBitmap - -We encourage anyone trying to get a deeper understanding of the Uniswap protocol to read the Uniswap V3 Book. - -## Calculating the memory positions of all Ticks - -Now that we fetched all **bitMaps**, we check which ticks are initialized and calculate the **tick position** from the **word index** and the **tickSpacing** of the pool. - -We check if a tick is **initialized** inside the word by shifting a bit by the index we are looking at and performing a bitwise AND operation: - -```typescript -const bit = 1n -const initialized = (bitmap & (bit << BigInt(i))) !== 0n -``` - -If the tick is **initialized**, we revert the compression from tick to word we made earlier by multiplying the word index with 256, which is the same as left shifting by 8 bit, adding the position we are currently at, and multiplying with the tickSpacing: - -```typescript -const tickIndex = (ind * 256 + i) * tickSpacing -``` - -The whole loop looks like this: - -```typescript -const tickIndices: number[] = [] - - for (let j = 0; j < wordPosIndices.length; j++) { - const ind = wordPosIndices[j] - const bitmap = results[j] - - if (bitmap !== 0n) { - for (let i = 0; i < 256; i++) { - const bit = 1n - const initialized = (bitmap & (bit << BigInt(i))) !== 0n - if (initialized) { - const tickIndex = (ind * 256 + i) * tickSpacing - tickIndices.push(tickIndex) - } - } - } - } -``` - -We now have an array containing the indices of all initialized Ticks. - -## Fetching all Ticks by their indices - -We use the multicallProvider again to execute an aggregated read call for all tick indices. -We create an array of call Promises again and use `.all()` to make our multicall: - -```typescript -const calls: any[] = [] - -for (const index of tickIndices) { - calls.push(poolContract.ticks(index)) -} - -const results = await multicallProvider.all(calls) -``` - -Again, the order of the results array is the same as the elements in **tickIndices**. - -We are able to combine the **tickIndices** and **results** array to create an array of `Tick` objects: - -```typescript -const allTicks: Tick[] = [] - - for (let i = 0; i < tickIndices.length; i++) { - const index = tickIndices[i] - const ethersResponse = results[i] - const tick = new Tick({ - index, - liquidityGross: JSBI.BigInt(ethersResponse.liquidityGross.toString()), - liquidityNet: JSBI.BigInt(ethersResponse.liquidityNet.toString()), - }) - allTicks.push(tick) - } -``` - -We need to parse the response from our RPC provider to JSBI values that the v3-sdk can work with. - -## Constructing the Pool - -We have everything to construct our `Pool` now: - -```typescript -const usdcWethPool = new Pool( - USDC, - WETH, - feeAmount, - slot0.sqrtPriceX96, - liquidity, - slot0.tick, - allTicks -) -``` - -With this fully initialized Pool, we can make accurate offchain calculations. - -## Next Steps - -Now that you are familiar with fetching Pool data, continue your journey with the [next example](./03-active-liquidity.md) on visualizing the Liquidity density of a pool. diff --git a/docs/sdk/v3/guides/advanced/04-price-oracle.md b/docs/sdk/v3/guides/advanced/03-price-oracle.md similarity index 100% rename from docs/sdk/v3/guides/advanced/04-price-oracle.md rename to docs/sdk/v3/guides/advanced/03-price-oracle.md diff --git a/docs/sdk/v3/guides/advanced/05-range-orders.md b/docs/sdk/v3/guides/advanced/04-range-orders.md similarity index 99% rename from docs/sdk/v3/guides/advanced/05-range-orders.md rename to docs/sdk/v3/guides/advanced/04-range-orders.md index a1f7f30376..ece8d30c65 100644 --- a/docs/sdk/v3/guides/advanced/05-range-orders.md +++ b/docs/sdk/v3/guides/advanced/04-range-orders.md @@ -81,13 +81,12 @@ To create our Position, we need to first decide the Tick Range that we want to p ### Upper Tick -We [create a Pool](./02-pool-data.md) that represents the V3 Pool we are interacting with and get the `token0Price`. +We create a Pool that represents the V3 Pool we are interacting with and get the `token0Price`. We won't need full tick data in this example. ```typescript import { Pool } from '@uniswap/v3-sdk' -... const pool = new Pool(token0, token1, fee, sqrtPriceX96, liquidity, tickCurrent) const currentPrice = pool.token0Price diff --git a/docs/sdk/v3/guides/swaps/04-routing.md b/docs/sdk/v3/guides/swaps/04-routing.md index 1c4521c2cd..670553644d 100644 --- a/docs/sdk/v3/guides/swaps/04-routing.md +++ b/docs/sdk/v3/guides/swaps/04-routing.md @@ -123,9 +123,9 @@ Depending on our preferences and reason for the issue we could retry the request ## Swapping using a route -To be able to spend the tokens of a wallet, a smart contract first needs to get an approval from that wallet. +To be able to spend the tokens of a wallet, a smart contract first needs to get an approval from that wallet. ERC20 tokens have an `approve` function that accepts the address of the smart contract that we want to allow spending our tokens and the amount the smart contract should be allowed to spend. -The `executeQuotedSwapFromRoute()` function takes care of this, but possibly needs two blocks for the execution for this reason. +The `executeQuotedSwapFromRoute()` function takes care of this, but might need two blocks for the execution for this reason. ```typescript import { SwapRouter, TradeType } from '@uniswap/v3-sdk' From a8dc15c500610701d8a88e2d9fc24ba614fd879d Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Tue, 28 Nov 2023 14:07:21 +0100 Subject: [PATCH 41/52] Fix init pool inaccuracies in swap guides --- docs/sdk/v3/guides/swaps/02-trading.md | 83 ++++--------------- .../v3/guides/swaps/03-simulate-offchain.md | 4 +- docs/sdk/v3/guides/swaps/04-routing.md | 48 ++++++++--- 3 files changed, 55 insertions(+), 80 deletions(-) diff --git a/docs/sdk/v3/guides/swaps/02-trading.md b/docs/sdk/v3/guides/swaps/02-trading.md index 63da6938ed..1020d6fa4a 100644 --- a/docs/sdk/v3/guides/swaps/02-trading.md +++ b/docs/sdk/v3/guides/swaps/02-trading.md @@ -109,78 +109,29 @@ If you cannot see the Tokens traded in your wallet, you possibly have to [import ## Constructing a route from pool information To construct our trade, we will first create a model instance of a `Pool`. We create an **ethers** provider like in the [previous guide](./01-quoting.md). -We will first extract the needed metadata from the relevant pool contract. Metadata includes both constant information about the pool as well as information about its current state stored in its first slot. -To interact with the Pool, we need to compute its contract address. We use the `computePoolAddress()` function to achieve that: +The sdk has a utility function to create a Pool from onchain data: ```typescript -import { computePoolAddress } from '@uniswap/v3-sdk' + import {Pool} from '@uniswap/v3-sdk' + import ethers from 'ethers' -const currentPoolAddress = computePoolAddress({ - factoryAddress: POOL_FACTORY_CONTRACT_ADDRESS, - tokenA: CurrentConfig.tokens.in, - tokenB: CurrentConfig.tokens.out, - fee: CurrentConfig.tokens.poolFee, -}) -``` - -Every Pool is uniquely identified by the two tokens it contains and its fee. -We can find the Pool Factory Contract address for our chain [here](https://docs.uniswap.org/contracts/v3/reference/deployments). - -We can now use the `getPoolData` function to fetch the metadata: - -```typescript -import { RPCPool } from '@uniswap/v3-sdk' -import { ethers } from 'ethers' - -const provider = new ethers.providers.JsonRpcProvider(CurrentConfig.rpc.mainnet) -const latestBlockNumber = await provider.getBlockNumber() - -const poolData = await RPCPool.getPoolData( - provider, - currentPoolAddress, - latestBlockNumber -) -``` + const provider = new ethers.providers.JsonRpcProvider(CurrentConfig.rpc.mainnet) -The `getPoolData()` function returns a `PoolData` object: - -```typescript -interface PoolData { - address: string - tokenA: Token - tokenB: Token - fee: FeeAmount - sqrtPriceX96: BigInt - liquidity: BigInt - tick: number - tickSpacing: number -} + const pool = await Pool.initFromChain( + provider, + CurrentConfig.tokens.in, + CurrentConfig.tokens.out, + CurrentConfig.tokens.poolFee + ) ``` -Before continuing, let's talk about the values we fetched here and what they represent: - -- `fee` is the fee that is taken from every swap that is executed on the pool in 1 per million - if the `fee` value of a pool is 500, ```500/ 1000000``` (or 0.05%) of the trade amount is taken as a fee. This fee goes to the liquidity providers of the Pool. -- `liquidity` is the amount of liquidity the Pool can use for trades at the current price. -- `sqrtPriceX96` is the current Price of the pool, encoded as a ratio between `token0` and `token1`. -- `tick` is the tick at the current price of the pool. - -Check out the [whitepaper](https://uniswap.org/whitepaper-v3.pdf) to learn more on how liquidity and ticks work in Uniswap V3. +Every Pool is uniquely identified by the two tokens it contains and its fee. +The initialized Pool already has all necessary metadata for our example but does not contain any Tick Data. +Fetching Ticks can be expensive for large pools and is not necessary for most use cases. +We will dive deeper into this topic in the [next guide](./03-simulate-offchain.md) You can find the full code in [`pool.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/src/libs/pool.ts). -Using this metadata along with our inputs, we will then construct a `Pool`: - -```typescript -const pool = new Pool( - CurrentConfig.tokens.in, - CurrentConfig.tokens.out, - poolData.fee, - poolData.sqrtPriceX96, - poolData.liquidity, - poolData.tick -) -``` - ## Creating a Route With this `Pool`, we can now construct a route to use in our trade. Routes represent a route over one or more pools from one Token to another. Let's imagine we have three pools: @@ -222,7 +173,7 @@ const amountOut = await getOutputQuote(swapRoute) ``` As shown below, the quote is obtained using the `v3-sdk`'s `SwapQuoter`, for this guide we use the `callQuoter()` function. -In contrast to the `quoteExactInputSingle()` function we used in the previous guide, this function works for any Route, not just a swap over a single Pool: +In contrast to the `quoteExactInputSingle()` function we used in the previous guide, this function works for a Route with any number of Uniswap V3 Pools, not just a swap over a single Pool: ```typescript import { SwapQuoter } from '@uniswap/v3-sdk' @@ -314,5 +265,5 @@ The return value is an `ethers.TransactionResponse` object. ## Next Steps -So far, we have used onchain calls to get a quote for our trades. -In the next guide on [offchain simulations](03-simulate-offchain.md), we will use the sdk to fetch Pool information first and simulate our Trades offchain. +So far, we have used onchain calls to get a quote for our trades. +In the next guide on [offchain simulations](03-simulate-offchain.md), we will use the sdk to fetch Tickdata first and simulate our Trades offchain. diff --git a/docs/sdk/v3/guides/swaps/03-simulate-offchain.md b/docs/sdk/v3/guides/swaps/03-simulate-offchain.md index 6bab605cd2..c7458cf82c 100644 --- a/docs/sdk/v3/guides/swaps/03-simulate-offchain.md +++ b/docs/sdk/v3/guides/swaps/03-simulate-offchain.md @@ -85,7 +85,7 @@ Check out the top pools on [Uniswap info](https://info.uniswap.org/#/pools). ## Initializing the Pools -We already initialized a Pool object in the previous guide using the `getPoolData` function. +We already initialized a Pool object in the previous guide using the `initFromChain` function. By default, a Pool does not hold the full Tickdata necessary to compute trades, as fetching it is not necessary for most use cases and can be expensive. To fetch the full Tickdata for the Pool, we use the `initializeTicks()` function on our Pools: @@ -110,7 +110,7 @@ Our pools are now fully initialized and we can use them to simulate our trade of ## Simulating trades -In this example, we want to make an exact Input trade from **USDC** to **DAI**. +In this example, we want to make an exact Input trade from **USDC** to **DAI**. We use the `Trade.bestTradeExactIn` function to find the best route given our Pools: ```typescript diff --git a/docs/sdk/v3/guides/swaps/04-routing.md b/docs/sdk/v3/guides/swaps/04-routing.md index 670553644d..0ae265bd98 100644 --- a/docs/sdk/v3/guides/swaps/04-routing.md +++ b/docs/sdk/v3/guides/swaps/04-routing.md @@ -71,7 +71,9 @@ const router = new AlphaRouter({ ## Creating a route -We will use the [SwapRouter02](https://github.com/Uniswap/v3-periphery/blob/v1.0.0/contracts/SwapRouter.sol) for our trade. +We will use the [SwapRouter02](https://github.com/Uniswap/swap-router-contracts/blob/main/contracts/SwapRouter02.sol) for our trade. +This is a different SwapRouter contract than the one we used in the previous example. +In contrast to the contract we used previously, this on can execute swaps on both V3 Pools and V2 Pairs. The `smart-order-router` package provides us with a `SwapOptionsSwapRouter02` interface, defining the wallet to use, slippage tolerance, and deadline for the transaction that we need to interact with the contract: ```typescript @@ -123,22 +125,44 @@ Depending on our preferences and reason for the issue we could retry the request ## Swapping using a route -To be able to spend the tokens of a wallet, a smart contract first needs to get an approval from that wallet. -ERC20 tokens have an `approve` function that accepts the address of the smart contract that we want to allow spending our tokens and the amount the smart contract should be allowed to spend. -The `executeQuotedSwapFromRoute()` function takes care of this, but might need two blocks for the execution for this reason. +First, we need to give approval to the `SwapRouter02` smart contract to spend our tokens for us: ```typescript -import { SwapRouter, TradeType } from '@uniswap/v3-sdk' - -wallet.connect(provider) +import { ethers } from 'ethers' +... -const txResponse = await executeQuotedSwapFromRoute( - route, - currencyAmountIn, - TradeType.EXACT_INPUT, - undefined, +const wallet = new ethers.Wallet(privateKey, provider) +const tokenContract = new ethers.Contract( + CurrentConfig.tokens.in.address, + ERC20ABI, wallet ) +const tokenApproval = await tokenContract.approve( + V3_SWAP_ROUTER_ADDRESS, + ethers.BigNumber.from(rawTokenAmountIn.toString()) +) +``` + +To be able to spend the tokens of a wallet, a smart contract first needs to get an approval from that wallet. +ERC20 tokens have an `approve` function that accepts the address of the smart contract that we want to allow spending our tokens and the amount the smart contract should be allowed to spend. + +We can get the **V3_SWAP_ROUTER_ADDRESS** for our chain from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). +Keep in mind that different chains might have **different deployment addresses** for the same contracts. +The deployment address for local forks of a network are the same as in the network you forked, so for a **fork of mainnet** it would be the address for **Mainnet**. + +We need to wait one block for the approval transaction to be included by the blockchain. + +Once the approval has been granted, we can now execute the trade using the route's computed calldata, values, and gas values: + +```typescript +const txRes = await wallet.sendTransaction({ + data: route.methodParameters.calldata, + to: V3_SWAP_ROUTER_ADDRESS, + value: route.methodParameters.value, + from: wallet.address, + maxFeePerGas: MAX_FEE_PER_GAS, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS, +}) ``` After swapping, you should see the currency balances update in the UI shortly after the block is confirmed. From eeda3aca7c9d3583aa3f0c83373660a727ec4a7f Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Mon, 4 Dec 2023 18:16:01 +0100 Subject: [PATCH 42/52] Update minting positions guide --- .../guides/liquidity/02-minting-position.md | 174 +++++------------- 1 file changed, 46 insertions(+), 128 deletions(-) diff --git a/docs/sdk/v3/guides/liquidity/02-minting-position.md b/docs/sdk/v3/guides/liquidity/02-minting-position.md index e14e7d1ecf..77a6e871e7 100644 --- a/docs/sdk/v3/guides/liquidity/02-minting-position.md +++ b/docs/sdk/v3/guides/liquidity/02-minting-position.md @@ -18,9 +18,8 @@ In the Uniswap V3 protocol, liquidity positions are represented using non-fungib The guide will **cover**: 1. Giving approval to transfer our tokens -2. Creating an instance of a `Pool` -3. Calculating our `Position` from our input tokens -4. Configuring and executing our minting transaction +2. Calculating our `Position` from our input tokens +3. Configuring and executing our minting transaction At the end of the guide, given the inputs above, we should be able to mint a liquidity position with the press of a button and view the position on the UI of the web application. @@ -36,132 +35,70 @@ The core code of this guide can be found in [`mintPosition()`](https://github.co We want to use the `NonfungiblePositionManager` contract to create our liqudity position. In situations where a smart contract is transfering tokens on our behalf, we need to give it approval to do so. -This is done by interacting with the Contract of the contract, considering ERC20 Tokens are smart contracts of their own. -Considering this, the first step to create our position is to give approval to the protocol's `NonfungiblePositionManager` to transfer our tokens: +We can use the `approveTokenTransfer()` function from the sdk for that: ```typescript -const token0Approval = await getTokenTransferApproval( +import { approveTokenTransfer } from '@uniswap/v3-sdk' +import { NONFUNGIBLE_POSITION_MANAGER_ADDRESSES } from '@uniswap/sdk-core' + +const signer = getWallet() + +const positionManagerAddress = NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[ + CurrentConfig.tokens.token0.chainId + ] + +const token0Approval = await approveTokenTransfer( + positionManagerAddress, token0Address, - amount0 + amount0, + signer ) -const token1Approval = await getTokenTransferApproval( +const token1Approval = await approveTokenTransfer( + positionManagerAddress, token1Address, - amount1 + amount1, + signer ) ``` -The logic to achieve that is wrapped in the `getTokenTransferApprovals` function. In short, since both **USDC** and **DAI** are ERC20 tokens, we setup a reference to their smart contracts and call the `approve` function: - -```typescript -import { ethers, BigNumber } from 'ethers' - -async function getTokenTransferApproval(address: string, amount: BigNumber) { - const provider = new ethers.providers.JsonRpcProvider(rpcUrl) - - const tokenContract = new ethers.Contract( - token.address, - ERC20_ABI, - provider - ) - - return tokenContract.approve( - NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, - amount - ) -} -``` - -We can get the Contract address for the NonfungiblePositionManager from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). -For Ethereum mainnet or a local fork of mainnet, we see that the contract address is `0xC36442b4a4522E871399CD717aBDD847Ab11FE88`. -In our example, this is defined in the [`constants.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/minting-position/src/libs/constants.ts) file. - -## Creating an instance of a `Pool` - -Having approved the transfer of our tokens, we now need to get data about the pool for which we will provide liquidity, in order to instantiate a Pool class. - -To start, we compute our Pool's address by using a helper function and passing in the unique identifiers of a Pool - the **two tokens** and the Pool **fee**. -The **fee** input parameter represents the swap fee that is distributed to all in range liquidity at the time of the swap. - -```typescript -import { computePoolAddress, FeeAmount } from '@uniswap/v3-sdk' -import { Token } from '@uniswap/sdk-core' - -const token0: Token = ... -const token1: Token = ... -const fee: FeeAmount = ... -const POOL_FACTORY_CONTRACT_ADDRESS: string = ... - -const currentPoolAddress = computePoolAddress({ - factoryAddress: POOL_FACTORY_CONTRACT_ADDRESS, - tokenA: token0, - tokenB: token1, - fee: poolFee, -}) -``` +We can get the Contract address for the NonfungiblePositionManager from the `NONFUNGIBLE_POSITION_MANAGER_ADDRESSES` in the sdk-core. -Again, we can get the factory contract address from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). -For Ethereum mainnet, or a local fork of mainnet, it is `0x1F98431c8aD98523631AE4a59f267346ea31F984`. -In our example, it is defined in [`constants.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/minting-position/src/libs/constants.ts) +## Calculating our `Position` from our input tokens -Then, we get the Pool's data by creating a reference to the Pool's smart contract and accessing its methods, very similar to what we did in the [Quoting guide](../swaps/01-quoting.md#referencing-the-pool-contract-and-fetching-metadata): +To create our Position, we first need to instantiate a `Pool` object: ```typescript -import IUniswapV3PoolABI from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json' - -const poolContract = new ethers.Contract( - currentPoolAddress, - IUniswapV3PoolABI.abi, - provider -) - -const [liquidity, slot0] = - await Promise.all([ - poolContract.liquidity(), - poolContract.slot0(), - ]) -``` + import {Pool} from '@uniswap/v3-sdk' -Having collected the required data, we can now create an instance of the `Pool` class: + const provider = getProvider() -```typescript -import { Pool } from '@uniswap/v3-sdk' - -const configuredPool = new Pool( - token0, - token1, - poolFee, - slot0.sqrtPriceX96.toString(), - liquidity.toString(), - slot0.tick -) + const pool = await Pool.initFromChain( + provider, + CurrentConfig.tokens.in, + CurrentConfig.tokens.out, + CurrentConfig.tokens.poolFee + ) ``` -We need a Pool instance to create our Position as various parameters of liquidity positions depend on the state of the Pool where they are created. -An example is the current price (named *sqrtPriceX96* after the way it is encoded) to know the ratio of the two Tokens we need to send to the Pool. - -Liquidity provided below the current Price will be provided in the first Token of the Pool, while liquidity provided above the current Price is made up by the second Token. - -## Calculating our `Position` from our input tokens - -Having created the instance of the `Pool` class, we can now use that to create an instance of a `Position` class, which represents the price range for a specific pool that LPs choose to provide in: +Next, we can use the pool to create an instance of a `Position` object, which represents a Liquidity Position offchain: ```typescript import { Position } from '@uniswap/v3-sdk' import { BigIntish } from '@uniswap/sdk-core' -// The maximum token amounts we want to provide. BigIntish accepts number, string or JSBI +// The maximum token amounts we want to provide. BigIntish accepts number, string or bigint const amount0: BigIntish = ... const amount1: BigIntish = ... const position = Position.fromAmounts({ - pool: configuredPool, + pool: pool, tickLower: - nearestUsableTick(configuredPool.tickCurrent, configuredPool.tickSpacing) - - configuredPool.tickSpacing * 2, + nearestUsableTick(pool.tickCurrent, pool.tickSpacing) - + pool.tickSpacing * 2, tickUpper: - nearestUsableTick(configuredPool.tick, configuredPool.tickSpacing) + - configuredPool.tickSpacing * 2, + nearestUsableTick(pool.tick, pool.tickSpacing) + + pool.tickSpacing * 2, amount0: amount0, amount1: amount1, useFullPrecision: true, @@ -177,20 +114,24 @@ Given those parameters, `fromAmounts` will attempt to calculate the maximum amou ## Configuring and executing our minting transaction -The Position instance is then passed as input to the `NonfungiblePositionManager`'s `addCallParameters` function. The function also requires an [`AddLiquidityOptions`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/nonfungiblePositionManager.ts#L77) object as its second parameter. This is either of type [`MintOptions`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/nonfungiblePositionManager.ts#L74) for minting a new position or [`IncreaseOptions`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/nonfungiblePositionManager.ts#L75) for adding liquidity to an existing position. For this example, we're using a `MintOptions` to create our position. +We can now use the `NonfungiblePositionManager` class to mint our Position: ```typescript import { MintOptions, NonfungiblePositionManager } from '@uniswap/v3-sdk' import { Percent } from '@uniswap/sdk-core' +const signer = getWallet() + const mintOptions: MintOptions = { - recipient: address, + recipient: getWalletAddress(), deadline: Math.floor(Date.now() / 1000) + 60 * 20, slippageTolerance: new Percent(50, 10_000), } // get calldata for minting a position -const { calldata, value } = NonfungiblePositionManager.addCallParameters( +const txResponse = NonfungiblePositionManager.mintOnChain( + signer, + provider, position, mintOptions ) @@ -202,30 +143,7 @@ The `MintOptions` interface requires three keys: - `deadline` defines the latest point in time at which we want our transaction to be included in the blockchain. - `slippageTolerance` defines the maximum amount of **change of the ratio** of the Tokens we provide. The ratio can change if for example **trades** that change the price of the Pool are included before our transaction. -The `addCallParameters` function returns the calldata as well as the value required to execute the transaction: - -```typescript -const transaction = { - data: calldata, - to: NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, - value: value, - from: address, - maxFeePerGas: MAX_FEE_PER_GAS, - maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS, -} -``` - -We use our wallet to send the transaction. As it is a write call, we need to sign the transaction with a valid private key. - -```typescript -const wallet = new ethers.Wallet(privateKey, provider) - -const txRes = await wallet.sendTransaction(transaction) -``` - -Write calls do not return the result of the transaction. If we want to read the result we would need to use for example `trace_transaction`. -You can find an example of that in the [Range Order guide](../advanced/05-range-orders.md). -In this example, we don't need the result of the transaction. +The `mitOnChain()` function directly signs and executes a transaction that will create our Position. The effect of the transaction is to mint a new Position NFT. We should see a new position with liquidity in our list of positions. From 3e658f8ec4ffe4c330e42edfe8d9d61e8eb84cbe Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Tue, 5 Dec 2023 15:48:08 +0100 Subject: [PATCH 43/52] Update fetching Positions guide --- .../guides/liquidity/03-fetching-positions.md | 136 ++++++------------ 1 file changed, 43 insertions(+), 93 deletions(-) diff --git a/docs/sdk/v3/guides/liquidity/03-fetching-positions.md b/docs/sdk/v3/guides/liquidity/03-fetching-positions.md index 744ecb6806..41998917a5 100644 --- a/docs/sdk/v3/guides/liquidity/03-fetching-positions.md +++ b/docs/sdk/v3/guides/liquidity/03-fetching-positions.md @@ -5,7 +5,7 @@ title: Fetching Positions ## Introduction -This guide will cover how to create (or mint) a liquidity position on the Uniswap V3 protocol. +This guide will cover how to fetch Positions from the Blockchain. Like the [Liquidity Position guide](./01-position-data.md) it doesn't have an accompanying example, nevertheless the concepts and functions used here can be found among the various examples that interact with liquidity positions. :::info @@ -17,125 +17,75 @@ In this guide, we will fetch **all Positions** an address has and fetch the **de The guide will **cover**: -1. Creating an ethersJS contract to interact with the NonfungiblePositionManager. -2. Fetching all positions for an address. -3. Fetching the position info for the positions. +1. Fetching all positions for an address. +2. Fetching the position info for the positions. -At the end of the guide, given the inputs above, we should be able to mint a liquidity position with the press of a button and view the position on the UI of the web application. +For this guide, the following Uniswap packages are used: -For this guide, we do not need to use the Uniswap SDKs, we will only import the contract ABI for the NonfungiblePositionManager Contract from [`@uniswap/v3-periphery`](https://www.npmjs.com/package/@uniswap/v3-periphery). +- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) +- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) -## Connecting to the NFTPositionManager Contract +## Fetching the number of Positions -We use **ethersJS** to interact with the NonfungiblePositionManager Contract. Let's create an ethers Contract: +We want to fetch all Positions for our address. +We first fetch the number of positions an address owns using the `getPositionCount` function: ```typescript -import { ethers } from 'ethers' -import INONFUNGIBLE_POSITION_MANAGER from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json' +import ethers from 'ethers' +import { Position } from '@uniswap/v3-sdk' -const provider = new ethers.providers.JsonRpcProvider(rpcUrl) +const provider = new ethers.providers.JsonRpcProvider('...rpcUrl') -const nfpmContract = new ethers.Contract( - NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, - INONFUNGIBLE_POSITION_MANAGER.abi, - provider -) -``` +// Address we want to fetch the positions for +const address = '...' -We get the Contract ABI from the 'v3-periphery` package and the contract address from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md) +const positionCount: bigint = await Position.getPositionCount(provider, address) +``` -## Fetching the Position Ids +Depending on the number of Positions this address owns, we may want to fetch them individually or all at once. -We want to fetch all Position Ids for our address. We first fetch the number of positions and then the ids by their indices. +## Fetching individual Positions -We fetch the number of positions using the `balanceOf` read call: +If we want to paginate the positions an address owns we can fetch them one by one using the `getPositionForAddressAndIndex` function: ```typescript +let index = 0 -const numPositions = await nfpmContract.balanceOf(address) +const position: Position = await getPositionForAddressAndIndex( + provider, + address, + index +) ``` -Next we iterate over the number of positions and fetch the ids: +We can use the position count we fetched earlier to create a loop for all positions we want to fetch. -```typescript -const calls = [] - -for (let i = 0; i < numPositions; i++) { - calls.push( - nfpmContract.tokenOfOwnerByIndex(address, i) - ) -} +## Fetching all Positions for an address -const positionIds = await Promise.all(calls) -``` +To fetch all Positions at once, we can use the `getAllPositionsForAddress` function: -## Fetching the Position Info - -Now that we have the ids of the Positions associated with our address, we can fetch the position info using the `positions` function. - -The solidity function returns a lot of values describing the Position: - -```solidity -function positions( - uint256 tokenId - ) external view returns ( - uint96 nonce, - address operator, - address token0, - address token1, - uint24 fee, - int24 tickLower, - int24 tickUpper, - uint128 liquidity, - uint256 feeGrowthInside0LastX128, - uint256 feeGrowthInside1LastX128, - uint128 tokensOwed0, - uint128 tokensOwed1 - ) +```typescript +const allPositions: Position[] = await getAllPositionsForAddress( + provider, + address +) ``` -In this example we only care about values needed to interact with positions, so we create an Interface `PositionInfo`: +This call may be too heavy for addresses that have hundreds of Positions. -```typescript -interface PositionInfo { - tickLower: number - tickUpper: number - liquidity: JSBI - feeGrowthInside0LastX128: JSBI - feeGrowthInside1LastX128: JSBI - tokensOwed0: JSBI - tokensOwed1: JSBI -} -``` +## Fetching a Position with its Id -We fetch the Position data with `positions`: +In some cases we may know the id of a specific Position, for example if we just created it. +We can use the `fetchWithPositionId` function to fetch the Position: ```typescript -const positionCalls = [] - -for (let id of positionIds) { - positionCalls.push( - nfpmContract.positions(id) - ) -} -const callResponses = await Promise.all(positionCalls) +const position: Position = await fetchWithPositionId( + provider, + positionId +) ``` -Finally, we map the RPC response to our interface: - -```typescript -const positionInfos = callResponses.map((position) => { - return { - tickLower: position.tickLower, - tickUpper: position.tickUpper, - liquidity: JSBI.BigInt(position.liquidity), - feeGrowthInside0LastX128: JSBI.BigInt(position.feeGrowthInside0LastX128), - feeGrowthInside1LastX128: JSBI.BigInt(position.feeGrowthInside1LastX128), - tokensOwed0: JSBI.BigInt(position.tokensOwed0), - tokensOwed1: JSBI.BigInt(position.tokensOwed1), - } -}) -``` +## Next Steps -We now have an array containing PositionInfo for all positions that our address holds. +Now that we know how to fetch Positions, let's move to the next guide on [modifying Positions](./04-modifying-position.md). From 1e204e50041ab1cf0f3c6b388daa5161de63918a Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Tue, 5 Dec 2023 16:02:00 +0100 Subject: [PATCH 44/52] Update Liquidity Positions guide --- .../v3/guides/liquidity/01-position-data.md | 162 +----------------- 1 file changed, 7 insertions(+), 155 deletions(-) diff --git a/docs/sdk/v3/guides/liquidity/01-position-data.md b/docs/sdk/v3/guides/liquidity/01-position-data.md index 0aa4bafd21..d9a4b04c03 100644 --- a/docs/sdk/v3/guides/liquidity/01-position-data.md +++ b/docs/sdk/v3/guides/liquidity/01-position-data.md @@ -5,21 +5,8 @@ title: Liquidity Positions ## Introduction -This guide will introduce us to **liquidity positions** in Uniswap V3 and present the `v3-sdk` classes and Contracts used to interact with the protocol. -The concepts and code snippets showcased here can be found across the **Pooling Liquidity** examples in the Uniswap code examples [repository](https://github.com/Uniswap/examples). - -In this guide, we will take a look at the [Position](../../reference/classes/Position.md) and [NonfungiblePositionManager](../../reference/classes/NonfungiblePositionManager.md) classes, as well as the [NonfungiblePositionManager Contract](../../../../contracts/v3/reference/periphery/NonfungiblePositionManager.md). - +This guide will introduce us to **liquidity positions** in Uniswap V3 and provide some theoretical background for the guides in this section. At the end of the guide, we should be familiar with the most important classes used to interact with liquidity positions. -We should also understand how to fetch positions from the **NonfungiblePositionManager Contract**. - -For this guide, the following Uniswap packages are used: - -- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) -- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) -- [`@uniswap/v3-periphery`](https://www.npmjs.com/package/@uniswap/v3-periphery) - -The code mentioned in this guide can be found across the [minting Position](https://github.com/Uniswap/examples/blob/main/v3-sdk/minting-position/src), [collecting Fees](https://github.com/Uniswap/examples/blob/main/v3-sdk/collecting-fees/src), [modifying positions](https://github.com/Uniswap/examples/blob/d34a53412dbf905802da2249391788a225719bb8/v3-sdk/modifying-position/src) and [swap and add liquidity](https://github.com/Uniswap/examples/blob/main/v3-sdk/swap-and-add-liquidity/src) examples. ## Prerequisites @@ -47,7 +34,7 @@ For example, a Pool with **HIGH** fee (1%) has a tickSpacing of 200, meaning the $$1.0001^{200} = 1.0202$$ or $$2.02$$% -### Liquidity Positions +## Liquidity Positions When someone provides liquidity to a Pool, they create a **Liquidity Position**. This position is defined by the amount of liquidity provided and the start tick and the end tick, or price range, of the Position. @@ -57,149 +44,14 @@ In this case, the liquidity provider will pay only one type of Token into the Po To learn more about how Ticks and Liquidity positions work, consider reading the [whitepaper](https://uniswap.org/whitepaper-v3.pdf) or the other resources mentioned above. -Now that we have a rough understanding of liquidity positions in Uniswap V3, let's look at the correspondent classes the SDK offers us. - -## Position class - -The **sdk** provides a [`Position`](https://github.com/Uniswap/v3-sdk/blob/main/src/entities/position.ts) class used to create local representations of an onchain position. -It is used to create the calldata for onchain calls to mint or modify an onchain position. - -There are four ways to construct a position. - -Directly with the [constructor](https://github.com/Uniswap/v3-sdk/blob/08a7c05/src/entities/position.ts#L40): - -```typescript -import { Pool, Position } from '@uniswap/v3-sdk' -import JSBI from 'jsbi' - -const pool = new Pool(...) -const tickLower: number = -100 -const tickUpper: number = 200 -const liquidity: JSBI = JSBI.BigInt('1000000000000000000') - -const position = new Position({ - pool, - liquidity, - tickLower, - tickUpper -}) -``` - -Using the [`fromAmounts()`](https://github.com/Uniswap/v3-sdk/blob/08a7c05/src/entities/position.ts#L312) function: - -```typescript -import { BigIntish } from '@uniswap/sdk-core' - -const pool = new Pool(...) -const tickLower: number = -100 -const tickUpper: number = 200 -const amount0: BigIntish = '1000000000000000000' -const amount1: BigIntish = JSBI.BigInt('1000000000000000000') -const useFullPrecision: boolean = true - -const position = Position.fromAmounts({ - pool, - tickLower, - tickUpper, - amount0, - amount1, - useFullPrecision -}) -``` - -Or using the [`fromAmount0()`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/entities/position.ts#L354) or [`fromAmount1()`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/entities/position.ts#L378) functions: - -```typescript -import { BigIntish } from '@uniswap/sdk-core' -... - -const pool = new Pool(...) -const tickLower: number = -200 -const tickUpper: number = 100 -const amount0: BigIntish = '1000000000000000000' -const useFullPrecision: boolean = true - -const singleSidePositionToken0 = Position.fromAmount0({ - pool, - tickLower, - tickUpper, - amount0, - useFullPrecision -}) - -const amount1: BigIntish = 100000000 - -const singleSidePositionToken1 = Position.fromAmount1({ - pool, - tickLower, - tickUpper, - amount1, - useFullPrecision -}) -``` - -These last two functions calculate a position at the given tick range given the amount of `token0` or `token1`. The amount of the second token is calculated from the ratio of the tokens inside the tick range and the amount of token one. - -A create transaction would then fail if the wallet doesn't hold enough `token1` or the Contract is not given the necessary **Transfer Approval**. - -All of these functions take an Object with **named values** as a call parameter. The amount and liquidity values are of type `BigIntish` which accepts `number`, `string` and `JSBI`. - -The values of `tickLower` and `tickUpper` must match **initializable ticks** of the Pool. - -## NonfungiblePositionManager - -The `NonfungiblePositionManager` class is mainly used to create calldata for functions on the **NonfungiblePositionManager Contract**. - -We will look at the **sdk** class and write functions on the Contract in this section. - -### Creating a Position - -To create a position on a Pool, the [`mint`](../../../../contracts/v3/reference/periphery/NonfungiblePositionManager.md#mint) function is called on the Contract. -The **sdk** class provides the `addCallParameters` function to create the calldata for the transaction: - -```typescript -import { MintOptions, NonfungiblePositionManager } from '@uniswap/v3-sdk' - -const mintOptions: MintOptions = { - recipient: address, - deadline: Math.floor(Date.now() / 1000) + 60 * 20, - slippageTolerance: new Percent(50, 10_000), -} - -// get calldata for minting a position -const { calldata, value } = NonfungiblePositionManager.addCallParameters( - positionToMint, - mintOptions -) -``` - -This call creates a position if it doesn't exist, but can also be used to increase an existing position. -Take a look at the [Mint Position guide](./02-minting-position.md) and [Modify Position guide](./04-modifying-position.md) to learn more. - -### Decreasing and Increasing a Position - -To decrease or increase the liquidity of a Position, the `decreaseLiquidity` or `increaseLiquidity` functions are called on the Contract. -To increase, `addCallParameters` is used as mentioned above, to decrease we use `removeCallParameters`: - -```typescript -const { calldata, value } = NonfungiblePositionManager.removeCallParameters( - currentPosition, - removeLiquidityOptions -) -``` - -Take a look at the [Modify Positions guide](04-modifying-position.md) to learn how to create the `currentPosition` and `removeLiquidityOptions` parameters. - -### Collecting Fees +The SDK wraps Liquidity Positions in the `Position` class. -To collect fees accrued, the `collect` function is called on the Contract. -The **sdk class** provides the `collectCallParameters` function to create the calldata for that: +## NFTPositionManager -```typescript -const { calldata, value } = - NonfungiblePositionManager.collectCallParameters(collectOptions) -``` +To simplify managing Liquidity Positions, Uniswap deployed the [NonfungiblePositionManager](../../../../contracts/v3/reference/periphery/NonfungiblePositionManager.md) contract. +The SDK includes the `NonfungiblePositionManager` class that wraps this contract and provides utility functions to interact with it. +We will use it to create and modify Positions in the following guides. ## Next steps From 5dfb98d414271b76c7f116abb860645d52c1c6dc Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Wed, 6 Dec 2023 16:31:10 +0100 Subject: [PATCH 45/52] Update Oracle guide --- .../sdk/v3/guides/advanced/03-price-oracle.md | 191 ++++++------------ 1 file changed, 65 insertions(+), 126 deletions(-) diff --git a/docs/sdk/v3/guides/advanced/03-price-oracle.md b/docs/sdk/v3/guides/advanced/03-price-oracle.md index 9b9bc9e7e4..b7c41201db 100644 --- a/docs/sdk/v3/guides/advanced/03-price-oracle.md +++ b/docs/sdk/v3/guides/advanced/03-price-oracle.md @@ -6,23 +6,21 @@ title: Uniswap as a Price Oracle ## Introduction This guide will cover how to fetch price observations from a V3 pool to get onchain asset prices. -It is based on the [Price Oracle example](https://github.com/Uniswap/examples/tree/main/v3-sdk/price-oracle), found in the Uniswap code examples [repository](https://github.com/Uniswap/example). -To run this example, check out the guide's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/price-oracle/README.md) and follow the setup instructions. +It is based on the [Price Oracle example](https://github.com/Uniswap/examples/tree/main/v3-sdk/oracle), found in the Uniswap code examples [repository](https://github.com/Uniswap/examples). +To run this example, check out the guide's [README](https://github.com/Uniswap/examples/blob/main/v3-sdk/oracle/README.md) and follow the setup instructions. :::info If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](./01-background.md) page! ::: -In this example we will use **ethers JS** to observe the development of a Pool's current tick over several blocks. +In this example we will observe the development of a Pool's current tick over several blocks. We will then calculate the time weighted average price - **TWAP**, and time weighted average liquidity - **TWAL** over the observed time interval. This guide will **cover**: -1. Understanding observations -2. Fetching observations -3. Computing TWAP -4. Computing TWAL -5. Why prefer observe over observations +1. Fetching observations +2. Computing TWAP +3. Computing TWAL Before diving into this guide, consider reading the theory behind using Uniswap V3 as an [Onchain Oracle](../../../../concepts/protocol/oracle.md). @@ -34,75 +32,64 @@ The core code of this guide can be found in [`oracle.ts`](https://github.com/Uni ## Understanding Observations -First, we need to create a Pool contract to fetch data from the blockchain. Check out the [Pool data guide](./02-pool-data.md) to learn how to compute the address and create an **ethers Contract** to interact with. +First, we need to create a Pool object to interact with the blockchain. +We use `initFromChain` to create a Pool with an RPC connection in the same manner we did in the Trading guides: ```typescript -const poolContract = new ethers.Contract( - poolAddress, - IUniswapV3PoolABI.abi, - provider +const provider = new ethers.providers.JsonRpcProvider( + '...rpcUrl' + ) + +const pool = Pool.initFromChain( + provider, + CurrentConfig.pool.token0, + CurrentConfig.pool.token1, + CurrentConfig.pool.fee ) ``` -All V3 pools store observations of the current tick and the block timestamp. +All V3 pools store observations of the current tick and the block timestamp. + To minimize pool deployment costs, only one Observation is stored in the contract when the Pool is created. Anyone who is willing to pay the gas costs can [increase](../../../../contracts/v3/reference/core/UniswapV3Pool.md#increaseobservationcardinalitynext) the number of stored observations to up to `65535`. -If the Pool cannot store an additional Observation, it overwrites the oldest one. - -We create an interface to map our data to: -```typescript -interface Observation { - secondsAgo: number - tickCumulative: bigint - secondsPerLiquidityCumulativeX128: bigint -} -``` +If the Pool cannot store an additional Observation, it overwrites the oldest one. -To fetch the `Observations` from our pool contract, we will use the [`observe`](../../../../contracts/v3/reference/core/UniswapV3Pool.md#observe) function: +## Number of Observations -```solidity -function observe( - uint32[] secondsAgos -) external view override noDelegateCall returns ( - int56[] tickCumulatives, - uint160[] secondsPerLiquidityCumulativeX128s -) -``` +Before fetching observations, we want to make sure the Pool stores enough observations to act as a reliable oracle. We first check how many observations are stored in the Pool by calling the `slot0` function. ```typescript -const slot0 = await poolContract.slot0() +const slot0 = await pool.rpcSlot0() const observationCount = slot0.observationCardinality const maxObservationCount = slot0.observationCardinalityNext ``` -The `observationCardinalityNext` is the maximum number of Observations the Pool **can store** at the moment. The `observationCardinality` is the actual number of Observations the Pool **has currently stored**. +The `observationCardinalityNext` is the maximum number of Observations the Pool **can store** at the moment. + Observations are only stored when the `swap()` function is called on the Pool or when a **Position is modified**, so it can take some time to write the Observations after the `observationCardinalityNext` was increased. If the number of Observations on the Pool is not sufficient, we need to call the `increaseObservationCardinalityNext()` function and set it to the value we desire. -This is a write function as the contract needs to store more data on the blockchain. -We will need a **wallet** or **signer** to pay the corresponding gas fee. +This is a write function because the contract needs to store more data on the blockchain. +We will need to pay the corresponding gas fee. In this example, we want to fetch 10 observations. ```typescript import { ethers } from 'ethers' -let provider = new ethers.providers.WebSocketProvider('rpcUrl...') let wallet = new ethers.Wallet('private_key', provider) +const observationCardinalityNext = 10 -const poolContract = new ethers.Contract( - poolAddress, - IUniswapV3PoolABI.abi, - wallet +const txRes = await pool.increaseObservationCardinalityNext( + wallet, + observationCardinalityNext ) - -const txRes = await poolContract.increaseObservationCardinalityNext(10) ``` The Pool will now fill the open Observation Slots. @@ -116,10 +103,36 @@ Because of this, we can be sure the oldest Observation is **at least** 10 blocks It is very likely that the number of blocks covered is bigger than 10. ::: +We are now sure that at least 10 observations exist, and can safely fetch observations for the last 10 blocks. + ## Fetching Observations -We are now sure that at least 10 observations exist, and can safely fetch observations for the last 10 blocks. -We call the `observe` function with an array of numbers, representing the timestamps of the Observations in seconds ago from now. +To fetch the `Observations` from our pool, we will use the [`observe`](../../../../contracts/v3/reference/core/UniswapV3Pool.md#observe) function: + +```solidity +function observe( + uint32[] secondsAgos +) external view override noDelegateCall returns ( + int56[] tickCumulatives, + uint160[] secondsPerLiquidityCumulativeX128s +) +``` + +The sdk wraps this function in the `rpcObserve` function on our Pool object. + +```typescript + const observeResponse = await pool.rpcObserve(timestamps) +``` + +Let's create an interface to map our data to: + +```typescript +interface Observation { + secondsAgo: number + tickCumulative: bigint + secondsPerLiquidityCumulativeX128: bigint +} +``` In this example, we calculate averages over the last ten blocks so we fetch 2 observations with 9 times the blocktime in between. Fetching an Observation `0s` ago will return the **most recent Observation** interpolated to the current timestamp as observations are written at most once a block. @@ -129,18 +142,19 @@ const timestamps = [ 0, 108 ] -const [tickCumulatives, secondsPerLiquidityCumulatives] = await poolContract.observe(timestamps) +const observeResponse = await pool.rpcObserve(timestamps) const observations: Observation[] = timestamps.map((time, i) => { return { - secondsAgo: time - tickCumulative: BigInt(tickCumulatives[i]) - secondsPerLiquidityCumulativeX128: BigInt(secondsPerLiquidityCumulatives[i]) + secondsAgo: time, + tickCumulative: observeResponse.tickCumulatives[i], + secondsPerLiquidityCumulativeX128: + observeResponse.secondsPerLiquidityCumulativeX128s[i], } }) ``` -We map the response from the RPC provider to match our Observations interface. +We map the response from the RPC provider to match our `Observation` interface. ## Calculating the average Price @@ -207,81 +221,6 @@ Adding massive amounts of liquidity to a Pool and withdrawing them after a block Use the **TWAP** with care and consider handling outliers. ::: -## Why prefer observe over observations? - -As touched on previously, the `observe` function calculates Observations for the timestamps requested from the nearest observations stored in the Pool. -It is also possible to directly fetch the stored observations by calling the `observations` function with the index of the Observation that we are interested in. - -Let's fetch all observations stored in our Pool. We already made sure the observationCardinality is 10. -The solidity struct `Observation` looks like this: - -```solidity -struct Observation { - // the block timestamp of the observation - uint32 blockTimestamp; - // the tick accumulator, i.e. tick * time elapsed since the pool was first initialized - int56 tickCumulative; - // the seconds per liquidity, i.e. seconds elapsed / max(1, liquidity) since the pool was first initialized - uint160 secondsPerLiquidityCumulativeX128; - // whether or not the observation is initialized - bool initialized; -} -``` - -It is possible to request any Observation up to (excluding) index `65535`, but indices equal to or greater than the `observationCardinality` will return uninitialized Observations. - -The full code to the following code snippets can be found in [`oracle.ts`](https://github.com/uniswap/examples/blob/main/v3-sdk/oracle/src/libs/oracle.ts) - -```typescript -let requests = [] -for (let i = 0; i < 10; i++) { - requets.push(poolContract.observations(i)) -} - -const results = await Promise.all(requests) -``` - -We can only request one Observation at a time, so we create an Array of Promises to get an Array of Observations. - -We already see one difference, to using the `observe` function here. -While `observe` creates an array onchain in the smart contract and returns it, calling `observations` requires us to make multiple RPC calls. - -:::note -Depending on our setup and the Node we are using, either option can be faster, but making multiple RPC calls always has the danger of the blockchain state changing between our calls. -While it is extremely unlikely, it is still possible that our Node updates with a new block and new Observation in between our calls. -Because we access indices of an array, this would give us an unexpected result that we need to handle as an edge case in our implementation. -::: - -One way to handle this behaviour is deploying or [using](https://github.com/mds1/multicall) a Contract with a [multicall](https://solidity-by-example.org/app/multi-call/) functionality to get all observations with one request. -You can also find an example of a JS multicall in the [Pool data guide](./02-pool-data.md). - -We map the RPC result to the Typescript interface that we created: - -```typescript -const utcNow = Math.floor(Date.now() / 1000) -const observations = results.map((result) => { - const secondsAgo = utcNow - Number(result.blockTimeStamp) - return { - secondsAgo, - tickCumulative: BigInt(result.tickCumulative), - secondsPerLiquidityCumulativeX128: BigInt(result.secondsPerLiquidityCumulativeX128) - } -}).sort((a, b) => a.secondsAgo - b.secondsAgo) -``` - -We now have an Array of observations in the same format that we are used to. - -:::note -Because Observations are stored in a **fixed size array** with always the oldest Observation overwritten if a new one is stored, they are **not sorted**. -We need to sort the result by the timestamp. -::: - -The timestamps of the Observations we got are correspondent to blocks where **Swaps or Position changes** happened on the Pool. -Because of this, we would need to calculate Observations for specific intervals manually from the **surrounding Observations**. - -In conclusion, it is much harder to work with `observations` than with `observe`, and we need to consider multiple edge cases. -For this reason, it is recommended to use the `observe` function. - ## Next Steps Now that you are familiar with the Oracle feature of Uniswap, consider checking out the [next guide](./05-range-orders.md) on **Range Orders**. From 79fd97852397db59a0839265b7d0e560779976e7 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Thu, 7 Dec 2023 16:01:38 +0100 Subject: [PATCH 46/52] Update range order guide to match updated example --- .../sdk/v3/guides/advanced/04-range-orders.md | 43 ++++++------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/docs/sdk/v3/guides/advanced/04-range-orders.md b/docs/sdk/v3/guides/advanced/04-range-orders.md index ece8d30c65..fc61b44c44 100644 --- a/docs/sdk/v3/guides/advanced/04-range-orders.md +++ b/docs/sdk/v3/guides/advanced/04-range-orders.md @@ -127,7 +127,7 @@ let targetTick = nearestUsableTick( ) ``` -This nearest Tick will most likely not **exactly** match our Price target. +This nearest Tick will most likely not **exactly** match our Price target but should be quite close. Depending on our personal preferences we can either err on the higher or lower side of our target by adding or subtracting the `tickSpacing` if the initializable Tick is lower or higher than the theoretically closest Tick. @@ -175,7 +175,7 @@ const position = Position.fromAmount0({ ``` Before we mint our position, we need to give the `NonfungiblePositionManager` Contract an approval to transfer our tokens. -We can find the Contract address on the official [Uniswap Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). +We can get the Contract address from the `sdk-core`. For local development, the contract address is the same as the network we are forking from. So if we are using a local fork of mainnet like described in the [Local development guide](../02-local-development.md), the contract address would be the same as on mainnet. @@ -328,18 +328,12 @@ We check if the tick has crossed our position, and if so we withdraw the Positio ## Closing the Limit Order -We call the NonfungiblePositionManager Contract with the `tokenId` to get all info of our position as we may have gotten fees from trades on the Pool: +We use the NonfungiblePositionManager together with the `tokenId` to get all info of our position as we may have gotten fees from trades on the Pool: ```typescript -import INON_FUNGIBLE_POSITION_MANAGER from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json' +import { NonfungiblePositionManager } from '@uniswap/v3-sdk' -const positionManagerContract = new ethers.Contract( - NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, - INONFUNGIBLE_POSITION_MANAGER.abi, - provider -) - -const positionInfo = await positionManagerContract.positions(tokenId) +const currentPosition = await NonfungiblePositionManager.fetchWithPositionId(getProvider(), tokenId) ``` We use the `NonfungiblePositionManager`, the `pool`, `positionInfo` and `tokenId` to create call parameter for a `decreaseLiquidity` call. @@ -349,19 +343,18 @@ We start with creating `CollectOptions`: ```typescript import { Percent, CurrencyAmount } from '@uniswap/sdk-core' import { CollectOptions, RemoveLiquidityOptions } from '@uniswap/v3-sdk' -import JSBI from 'jsbi' const collectOptions: Omit = { expectedCurrencyOwed0: CurrencyAmount.fromRawAmount( - pool.token0, - JSBI.BigInt(positionInfo.tokensOwed0.toString()) + CurrentConfig.tokens.token0, + 0 ), expectedCurrencyOwed1: CurrencyAmount.fromRawAmount( - pool.token1, - JSBI.BigInt(positionInfo.tokensOwed1.toString()) + CurrentConfig.tokens.token1, + 0 ), - recipient: wallet.address, -} + recipient: address, + } ``` Next we create `RemoveLiquidityOptions`. We remove all our liquidity so we set liquidityPercentage to `1`: @@ -377,24 +370,12 @@ const removeLiquidityOptions: RemoveLiquidityOptions = { } ``` -We create a new `Position` object from the updated `positionInfo` info we fetched: - -```typescript - -const updatedPosition = new Position{ - pool, - liquidity: JSBI.BigInt(currentPositionInfo.liquidity.toString()), - tickLower: currentPositionInfo.tickLower, - tickUpper: currentPositionInfo.tickUpper, -} -``` - We have everything to create our calldata now and are ready to make our Contract call: ```typescript const { calldata, value } = NonfungiblePositionManager.removeCallParameters( - updatedPosition, + currentPosition, removeLiquidityOptions ) const transaction = { From 1d58508227f48a7282f0d25754b9e1cb8d86956a Mon Sep 17 00:00:00 2001 From: Koray Koska <11356621+koraykoska@users.noreply.github.com> Date: Wed, 13 Dec 2023 20:19:49 +0400 Subject: [PATCH 47/52] feat: modifying positions guide with new sdk --- .../guides/liquidity/04-modifying-position.md | 272 +++++++++--------- 1 file changed, 139 insertions(+), 133 deletions(-) diff --git a/docs/sdk/v3/guides/liquidity/04-modifying-position.md b/docs/sdk/v3/guides/liquidity/04-modifying-position.md index 4e349321cd..bb8e902b6d 100644 --- a/docs/sdk/v3/guides/liquidity/04-modifying-position.md +++ b/docs/sdk/v3/guides/liquidity/04-modifying-position.md @@ -68,201 +68,207 @@ The `fractionToRemove` variable is the fraction of the Position that we want to ## Adding liquidity to our position -Assuming we have already minted a position, our first step is to construct the modified position using our original position to calculate the amount by which we want to increase our current position: +Assuming we have already minted a position, our first step is to fetch that position, or reuse the `Position` object if it was just created. + +To fetch a position using the id you will need an initialized ethers provider for the RPC you are using. +For the local fork configuration mentioned above you can use `http://localhost:8545`. + +Use the following snippet to fetch a position using the position id: ```typescript -const fractionToAdd: number = ... +import { ethers } from 'ethers' +import { Position } from '@uniswap/v3-sdk' -const amount0Increased: JSBI = fromReadableAmount( - readableAmount0 * fractionToAdd, - token0.decimals -) -const amount1Increase: JSBI = fromReadableAmount( - readableAmount1 * fractionToAdd, - token1.decimals -) +// You need to know this, or fetch the position differently +const myPositionId = "0xabcdef" -const positionToIncreaseBy = constructPosition( - amount0Increased, - amount1Increase - ) -) +const ethersProvider = new ethers.providers.JsonRpcProvider("http://localhost:8545") +const position = await Position.fetchWithPositionId(ethersProvider, myPositionId) ``` -The `fromReadableAmount()` function calculates the amount of tokens in their smallest unit, so for example 1 ETH would be `1000000000000000000` Wei as ETH has 18 decimals. - -A better way to get the amounts might be to fetch them with the positionId directly from the blockchain. -We demonstrated how to do that in the [first guide](./01-position-data.md#fetching-positions) of this series. +For more examples on how to fetch your position, refer to the [Fetching Positions Guide](./03-fetching-positions.md). -```typescript -import { Pool, Position } from '@uniswap/v3-sdk' -import JSBI from 'jsbi' - -function constructPosition( - amount0: JSBI, - amount1: JSBI -): Position { - // create Pool same as in the previous guide - const pool = new Pool(...) - - // create position using the maximum liquidity from input amounts - return Position.fromAmounts({ - pool, - tickLower: - nearestUsableTick(pool.tickCurrent, pool.tickSpacing) - - pool.tickSpacing * 2, - tickUpper: - nearestUsableTick(pool.tickCurrent, pool.tickSpacing) + - pool.tickSpacing * 2, - amount0, - amount1, - useFullPrecision: true, - }) -} -``` +The easiest version of increasing your position is if you know a percentage by which you want to increase it. +If you increase it by 10%, it means both current token balances (amount0, amount1) in the position will be increased by 10% each. -The function receives two arguments, which are the amounts that are used to construct the Position instance. In this example, both of the arguments follow the same logic: we multiply the parameterized `tokenAmount` by the parameterized `fractionToAdd` since the new liquidity position will be added on top of the already minted liquidity position. +So if you have a position with 1000 USDC and 1000 USDT, the 10% increase will increase both to 1100. +This also means you need to make approvals to the `NonfungiblePositionManager` before to reflect at least those 100 USDC and USDT changes, as mentioned in the beginning of this guide. The exact amount can be calculated using current amount0 and amount1 of your position and the percentage you are using. -We then need to construct an options object of type [`AddLiquidityOptions`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/nonfungiblePositionManager.ts#L77) similar to how we did in the minting case. In this case, we will use [`IncreaseOptions`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/nonfungiblePositionManager.ts#L75): +The snippet to increase liquidity by 10% can be seen below: ```typescript -import { AddLiquidityOptions } from '@uniswap/v3-sdk' +import { ethers } from 'ethers' +import { Fraction } from '@uniswap/sdk-core' +// Use your private key or another way to initialize a Wallet. +// A different way would be to use the Metamask signer in the browser. +const myWallet = new ethers.Wallet("0x_private_key") + +const ethersProvider = new ethers.providers.JsonRpcProvider("http://localhost:8545") + +// Either you know this already like mentioned before, or you can access it from the fetched position with +// position.positionId +const positionId = "0xabcdef" const addLiquidityOptions: AddLiquidityOptions = { deadline: Math.floor(Date.now() / 1000) + 60 * 20, slippageTolerance: new Percent(50, 10_000), - tokenId, + tokenId: positionId, } -``` -Compared to minting, we have we have omitted the `recipient` parameter and instead passed in the `tokenId` of the position we previously minted. -As the Position already exists, the recipient doesn't change, instead the NonfungiblePositionManager contract can modify the existing Position by accessing it with its id. +const transactionResponse = await Position.increasePositionByPercentageOnChain( + myWallet, + ethersProvider, + new Fraction(10, 100), // (10 / 100) for 10%tokenId + addLiquidityOptions +) -The tokenId can be fetched with the tokenOfOwnerByIndex function of the NonfungiblePositionManager Contract as described [here](./01-position-data.md#fetching-positions). +// Wait for 3 confirmations and then access the transaction receipt. +const transactionReceipt = await transactionResponse.wait(3) +``` -The newly created position along with the options object are then passed to the `NonfungiblePositionManager`'s `addCallParameters`: +In certain case you might want to increase your position to a specific value. -```typescript -import { NonfungiblePositionManager } from '@uniswap/v3-sdk' +Let's assume you display position data in a UI to the user and they can freely change the amounts to which they want to change the position amounts. +Whenever the user changes one of the values (amount0 or amount1), you will need to automatically update the other value as positions can only be changed +in percentage increments on both sides as they need to be balanced. -const positionToIncreaseBy = constructPosition(CurrentConfig.tokens.amount0, CurrentConfig.tokens.amount1) +If you display a position with the following values: -const { calldata, value } = NonfungiblePositionManager.addCallParameters( - positionToIncreaseBy, - addLiquidityOptions -) +``` +Pool: USDC <> WETH +amount0: 1000 USDC +amount1: 0.5 WETH ``` -The return values of `addCallParameters` are the calldata and value of the transaction we need to submit to increase our position's liquidity. We can now build and execute the transaction: +And the user wants to change `amount0` to 1500 USDC, you would run a calculation like the following: ```typescript -import { ethers } from 'ethers' +const currentAmount0 = myPosition.amount0 // USDC +const currentAmount1 = myPosition.amount1 // WETH -const transaction = { - data: calldata, - to: NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, - value: value, - from: address, - maxFeePerGas: MAX_FEE_PER_GAS, - maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS, -} +// User asked for 1500 USDC. Fractions are initialized with the smallest amount +// So we need to multiply 1500 by 10^decimals +const wantedAmount0 = CurrencyAmount.fromFractionalAmount(myPosition.pool.token0, 1500n * (10n ** BigInt(myPosition.pool.token0.decimals)), 1) + +// First make sure that wantedAmount0 is higher than currentAmount0, otherwise you will need to follow +// the next section of this guide to decrease a position. + +const positionMultiplier = wantedAmount0.divide(currentAmount0).asFraction + +const resultingAmount1 = currentAmount1.multiply(positionMultiplier) -const wallet = new ethers.Wallet(privateKey, provider) +// We can now display resultingAmount1 to the user, so he knows both amounts before confirming the position change. -const txRes = await wallet.sendTransaction(transaction) +// 1500 / 1000 = 1.5, so we need to subtract 1 to get 0.5, which is the format the increase position function expects +const percentageIncrease = positionMultiplier.subtract(new Fraction(1, 1)) ``` -We can get the Contract address for the NonfungiblePositionManager from [Github](https://github.com/Uniswap/v3-periphery/blob/main/deploys.md). +Using this `percentageIncrease` Fraction you can follow the snippets from before to increase the position by this percentage. -After pressing the button, note how the balance of USDC and DAI drops and our position's liquidity increases. +Make sure to approve the `NonfungiblePositionManager` by the difference of current amounts and wanted amounts for both tokens. If you made an unlimited +approval for minting the position, you can skip this step. ## Removing liquidity from our position -The `removeLiquidity` function is the mirror action of adding liquidity and will be somewhat similar as a result, requiring a position to already be minted. +Assuming we have already minted a position, our first step is to fetch that position, or reuse the `Position` object if it was just created. -To start, we create a position identical to the one we minted: +To fetch a position using the id you will need an initialized ethers provider for the RPC you are using. +For the local fork configuration mentioned above you can use `http://localhost:8545`. + +Use the following snippet to fetch a position using the position id: ```typescript -const amount0: JSBI = fromReadableAmount( - readableAmount0 * fractionToAdd, - token0.decimals -) -const amount1: JSBI = fromReadableAmount( - readableAmount1 * fractionToAdd, - token1.decimals -) +import { ethers } from 'ethers' +import { Position } from '@uniswap/v3-sdk' -const currentPosition = constructPosition( - amount0, - amount1 -) +// You need to know this, or fetch the position differently +const myPositionId = "0xabcdef" + +const ethersProvider = new ethers.providers.JsonRpcProvider("http://localhost:8545") +const position = await Position.fetchWithPositionId(ethersProvider, myPositionId) ``` -We then need to construct an options object of type [`RemoveLiquidityOptions`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/nonfungiblePositionManager.ts#L138): +For more examples on how to fetch your position, refer to the [Fetching Positions Guide](./03-fetching-positions.md). + +The easiest version of increasing your position is if you know a percentage by which you want to decrease it. +If you decrease it by 10%, it means both current token balances (amount0, amount1) in the position will be decreased by 10% each. + +So if you have a position with 1000 USDC and 1000 USDT, the 10% decrease will decrease both to 900. + +If you want to completely close a position, pass 100% to this function. + +The snippet to decrease liquidity by 10% can be seen below: ```typescript -import { RemoveLiquidityOptions } from '@uniswap/v3-sdk' -import { Percent } from '@uniswap/sdk-core' +import { ethers } from 'ethers' +import { Fraction } from '@uniswap/sdk-core' -const removeLiquidityOptions: RemoveLiquidityOptions = { +// Use your private key or another way to initialize a Wallet. +// A different way would be to use the Metamask signer in the browser. +const myWallet = new ethers.Wallet("0x_private_key") + +const ethersProvider = new ethers.providers.JsonRpcProvider("http://localhost:8545") + +// Either you know this already like mentioned before, or you can access it from the fetched position with +// position.positionId +const positionId = "0xabcdef" +const decreaseLiquidityOptions: Omit = { deadline: Math.floor(Date.now() / 1000) + 60 * 20, slippageTolerance: new Percent(50, 10_000), tokenId: positionId, - // percentage of liquidity to remove - liquidityPercentage: new Percent(0.5), - collectOptions, } + +const transactionResponse = await Position.decreasePositionByPercentageOnChain( + myWallet, + ethersProvider, + new Fraction(10, 100), // (10 / 100) for 10%tokenId + decreaseLiquidityOptions +) + +// Wait for 3 confirmations and then access the transaction receipt. +const transactionReceipt = await transactionResponse.wait(3) ``` -Just as with adding liquidity, we have we have omitted the `recipient` parameter and instead passed in the `tokenId` of the position we previously minted. +In certain case you might want to decrease your position to a specific value. -We have also provide two additional parameters: +Let's assume you display position data in a UI to the user and they can freely change the amounts to which they want to change the position amounts. +Whenever the user changes one of the values (amount0 or amount1), you will need to automatically update the other value as positions can only be changed +in percentage increments on both sides as they need to be balanced. -- `liquidityPercentage` determines how much liquidity is removed from our initial position (as a `Percentage`), and transfers the removed liquidity back to our address. We set this percentage from our guide configuration ranging from 0 (0%) to 1 (100%). In this example we would remove 50% of the liquidity. -- [`collectOptions`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/nonfungiblePositionManager.ts#L105) gives us the option to collect the fees, if any, that we have accrued for this position. In this example, we won't collect any fees, so we provide zero values. If you'd like to see how to collect fees without modifying your position, check out our [collecting fees](./03-collecting-fees.md) guide! +If you display a position with the following values: -```typescript -import { CurrencyAmount } from '@uniswap/sdk-core' -import { CollectOptions } from '@uniswap/v3-sdk' - -const collectOptions: Omit = { - expectedCurrencyOwed0: CurrencyAmount.fromRawAmount( - token0, - 0 - ), - expectedCurrencyOwed1: CurrencyAmount.fromRawAmount( - token1, - 0 - ), - recipient: address, -} +``` +Pool: USDC <> WETH +amount0: 1000 USDC +amount1: 0.5 WETH ``` -The position object along with the options object is passed to the `NonfungiblePositionManager`'s `removeCallParameters`, similar to how we did in the adding liquidity case: +And the user wants to change `amount0` to 800 USDC, you would run a calculation like the following: ```typescript -const { calldata, value } = NonfungiblePositionManager.removeCallParameters( - currentPosition, - removeLiquidityOptions -) -``` +const currentAmount0 = myPosition.amount0 // USDC +const currentAmount1 = myPosition.amount1 // WETH -The return values `removeCallParameters` are the calldata and value that are needed to construct the transaction to remove liquidity from our position. We can build the transaction and send it for execution: +// User asked for 800 USDC. Fractions are initialized with the smallest amount +// So we need to multiply 1500 by 10^decimals +const wantedAmount0 = CurrencyAmount.fromFractionalAmount(myPosition.pool.token0, 800n * (10n ** BigInt(myPosition.pool.token0.decimals)), 1) -```typescript -const transaction = { - data: calldata, - to: NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, - value: value, - from: address, - maxFeePerGas: MAX_FEE_PER_GAS, - maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS, -} +// First make sure that wantedAmount0 is lower than currentAmount0, otherwise you will need to follow +// the previous section of this guide to increase a position. + +const positionMultiplier = wantedAmount0.divide(currentAmount0).asFraction -const txRes = await wallet.sendTransaction(transaction) +const resultingAmount1 = currentAmount1.multiply(positionMultiplier) + +// We can now display resultingAmount1 to the user, so he knows both amounts before confirming the position change. + +// 800 / 1000 = 0.8, so we need to do 1 - 0.8 to get 0.2, which is the format the decrease position function expects +const percentageDecrease = new Fraction(1, 1).subtract(positionMultiplier) ``` -After pressing the button, note how the balance of USDC and DAI increases and our position's liquidity drops. +Using this `percentageDecrease` Fraction you can follow the snippets from before to decrease the position by this percentage. + +For decreasing positions, no approvals are required. ## Next Steps From 50cf87f87303ccdd2be96efd9871ce4b82a1aefc Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Thu, 14 Dec 2023 18:34:02 +0100 Subject: [PATCH 48/52] Update collect fees guide to match new sdk version --- .../v3/guides/liquidity/05-collecting-fees.md | 83 +++---------------- 1 file changed, 12 insertions(+), 71 deletions(-) diff --git a/docs/sdk/v3/guides/liquidity/05-collecting-fees.md b/docs/sdk/v3/guides/liquidity/05-collecting-fees.md index b2860165ab..2ccb59d2e6 100644 --- a/docs/sdk/v3/guides/liquidity/05-collecting-fees.md +++ b/docs/sdk/v3/guides/liquidity/05-collecting-fees.md @@ -11,12 +11,11 @@ This guide will cover how to collect fees from a liquidity position on the Unisw If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](../01-background.md) page! ::: -In the Uniswap V3 protocol, liquidity positions are represented using non-fungible tokens. In this guide we will use the `NonfungiblePositionManager` class to help us mint a liquidity position for the **USDC - DAI** pair. We will then attempt to collect any fees that the position has accrued from those trading against our provisioned liquidity. The inputs to our guide are the **two tokens** that we are pooling for, the **amount** of each token we are pooling for, the Pool **fee** and the **max amount of accrued fees** we want to collect for each token. +In the Uniswap V3 protocol, liquidity positions are represented using non-fungible tokens. In this guide we will use the `NonfungiblePositionManager` class to help us mint a liquidity position for the **USDC - DAI** pair. We will then attempt to collect any fees that the position has accrued from those trading against our provisioned liquidity. The inputs to our guide are the **two tokens** that we are pooling for, the **amount** of each token we are pooling for, the Pool **fee** and the **percentage of our accrued fees** we want to collect from the pool. The guide will **cover**: -1. Setting up our fee collection -2. Submitting our fee collection transaction +1. Collecting Fees from a position At the end of the guide, given the inputs above, we should be able to collect the accrued fees (if any) of a minted position with the press of a button and see the change reflected in our position and the balance of our tokens. @@ -40,82 +39,24 @@ All of the fee collecting logic can be found in the [`collectFees`](https://gith To start, we fetch the position from the NonfungiblePositionManager Contract to get the fees we are owed: ```typescript -import { ethers } from 'ethers' -import JSBI from 'jsbi' -... +import { Position } from '@uniswap/v3-sdk' -const nfpmContract = new ethers.Contract(NONFUNGIBLE_POSITION_MANAGER_ADDRESS, provider) -const position = nfpmContract.positions(positionId) -``` - -Next, we construct an options object of type [`CollectOptions`](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/nonfungiblePositionManager.ts#L105) that holds the data about the fees we want to collect: - -```typescript -import { CurrencyAmount } from '@uniswap/sdk-core' - -const collectOptions: CollectOptions = { - tokenId: positionId, - expectedCurrencyOwed0: CurrencyAmount.fromRawAmount( - CurrentConfig.tokens.token0, - JSBI.BigInt(position.tokensOwed0) - ), - expectedCurrencyOwed1: CurrencyAmount.fromRawAmount( - CurrentConfig.tokens.token1, - JSBI.BigInt(position.tokensOwed1) - ), - recipient: address, -} +const position = await Position.fetchWithPositionId(provider, positionId) ``` Read more about fetching position info [here](./01-position-data.md#fetching-positions). -Similar to the other functions exposed by the `NonfungiblePositionManager`, we pass the `tokenId` and the `recipient` of the fees, which in this case is our function's input position id and our wallet's address. - -The other two `CurrencyAmount` parameters (`expectedCurrencyOwed0` and `expectedCurrencyOwed1`) define the **maximum** amount of currency we expect to get collect through accrued fees of each token in the pool. We set these through our guide's configuration. - -In a real world scenario, we can fetch the amount of fees that are owed to the Position through the `positions()` function of the NonfungiblePositionManager Contract. -We fetch the position info like in this code snippet taken from the [Fetching Positions guide](./03-fetching-positions.md): - -```typescript -const positionInfos = callResponses.map((position) => { - return { - tickLower: position.tickLower, - tickUpper: position.tickUpper, - liquidity: JSBI.BigInt(position.liquidity), - feeGrowthInside0LastX128: JSBI.BigInt(position.feeGrowthInside0LastX128), - feeGrowthInside1LastX128: JSBI.BigInt(position.feeGrowthInside1LastX128), - tokensOwed0: JSBI.BigInt(position.tokensOwed0), - tokensOwed1: JSBI.BigInt(position.tokensOwed1), - } -}) -``` - -The `tokensOwed0` and `tokensOwed1` values are the fees owed. - -In this example, we have the values hardcoded in the [`config.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/collecting-fees/src/config.ts) file. - -## Submitting our fee collection transaction - -Next, we get the call parameters for collecting our fees from our `NonfungiblePositionManager` using the constructed `CollectOptions`: +Next, we specify the percentage of fees that we want to collect and use the sdk to collect our fees. +We want to collect all fees in this example: ```typescript -const { calldata, value } = - NonfungiblePositionManager.collectCallParameters(collectOptions) -``` +const percentageToCollect = new Percent(1) -The function above returns the calldata and value required to construct the transaction for collecting accrued fees. Now that we have both the calldata and value we needed for the transaction, we can build and execute the it: - -```typescript -const transaction = { - data: calldata, - to: NONFUNGIBLE_POSITION_MANAGER_CONTRACT_ADDRESS, - value: value, - from: address, - maxFeePerGas: MAX_FEE_PER_GAS, - maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS, -} - -const txRes = await wallet.sendTransaction(transaction) +const txResponse = await position.collectFeesOnChain({ + signer: wallet, + provider, + percentage: CurrentConfig.tokens.feePercentage, + }) ``` After pressing the button, if someone has traded against our position, we should be able to note how the balance of USDC and DAI increases as we collect fees. From f99d9ec3300603a603db98b7d0224c75d3876951 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Fri, 15 Dec 2023 15:17:37 +0100 Subject: [PATCH 49/52] Update swap-and-add-liquidity guide --- .../liquidity/06-swap-and-add-liquidity.md | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/sdk/v3/guides/liquidity/06-swap-and-add-liquidity.md b/docs/sdk/v3/guides/liquidity/06-swap-and-add-liquidity.md index be1b7d20d6..96b12e6d21 100644 --- a/docs/sdk/v3/guides/liquidity/06-swap-and-add-liquidity.md +++ b/docs/sdk/v3/guides/liquidity/06-swap-and-add-liquidity.md @@ -43,18 +43,22 @@ Also note that we do not need to give approval to the `NonfungiblePositionManage The first step is to approve the `SwapRouter` smart contract to spend our tokens for us in order for us to add liquidity to our position: ```typescript -const tokenInApproval = await getTokenTransferApproval( - token0, - V3_SWAP_ROUTER_ADDRESS -) +// Give approval to the router contract to transfer tokens + const tokenInApproval = await getTokenTransferApproval( + V3_SWAP_ROUTER_ADDRESS, + CurrentConfig.tokens.token0, + TOKEN_AMOUNT_TO_APPROVE_FOR_TRANSFER + ) -const tokenOutApproval = await getTokenTransferApproval( - token1, - V3_SWAP_ROUTER_ADDRESS -) + const tokenOutApproval = await getTokenTransferApproval( + V3_SWAP_ROUTER_ADDRESS, + CurrentConfig.tokens.token1, + TOKEN_AMOUNT_TO_APPROVE_FOR_TRANSFER + ) ``` We described the `getTokenTransferApproval` function [here](./02-minting-position.md#giving-approval-to-transfer-our-tokens). +We defined the address for the swaprouter in our `constants.ts` file. Then we can setup our router, the [`AlphaRouter`](https://github.com/Uniswap/smart-order-router/blob/97c1bb7cb64b22ebf3509acda8de60c0445cf250/src/routers/alpha-router/alpha-router.ts#L333), which is part of the [smart-order-router package](https://www.npmjs.com/package/@uniswap/smart-order-router). The router requires a `chainId` and a `provider` to be initialized. Note that routing is not supported for local forks, so we will use a mainnet provider even when swapping on a local fork: From bc709fca6d6fb5dbf4ac1b19982b20582c2b2468 Mon Sep 17 00:00:00 2001 From: Koray Koska <11356621+koraykoska@users.noreply.github.com> Date: Sun, 17 Dec 2023 22:16:00 +0400 Subject: [PATCH 50/52] fix: named params --- .../guides/liquidity/04-modifying-position.md | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/sdk/v3/guides/liquidity/04-modifying-position.md b/docs/sdk/v3/guides/liquidity/04-modifying-position.md index bb8e902b6d..ae1ec0945b 100644 --- a/docs/sdk/v3/guides/liquidity/04-modifying-position.md +++ b/docs/sdk/v3/guides/liquidity/04-modifying-position.md @@ -115,12 +115,12 @@ const addLiquidityOptions: AddLiquidityOptions = { tokenId: positionId, } -const transactionResponse = await Position.increasePositionByPercentageOnChain( - myWallet, - ethersProvider, - new Fraction(10, 100), // (10 / 100) for 10%tokenId - addLiquidityOptions -) +const transactionResponse = await position.increasePositionByPercentageOnChain({ + signer: myWallet, + provider: ethersProvider, + percentage: new Fraction(10, 100), // (10 / 100) for 10%tokenId + options: addLiquidityOptions +}) // Wait for 3 confirmations and then access the transaction receipt. const transactionReceipt = await transactionResponse.wait(3) @@ -218,12 +218,12 @@ const decreaseLiquidityOptions: Omit Date: Wed, 27 Dec 2023 17:18:39 +0100 Subject: [PATCH 51/52] Name parameters and import packages from uniswapfoundation in v3 sdk guides --- docs/sdk/v3/guides/02-local-development.md | 4 +- .../sdk/v3/guides/advanced/03-price-oracle.md | 30 ++++---- .../sdk/v3/guides/advanced/04-range-orders.md | 20 +++--- .../guides/liquidity/02-minting-position.md | 72 +++++++++---------- .../guides/liquidity/03-fetching-positions.md | 6 +- .../guides/liquidity/04-modifying-position.md | 12 ++-- .../v3/guides/liquidity/05-collecting-fees.md | 4 +- .../liquidity/06-swap-and-add-liquidity.md | 50 ++++++------- docs/sdk/v3/guides/swaps/01-quoting.md | 22 +++--- docs/sdk/v3/guides/swaps/02-trading.md | 40 +++++------ .../v3/guides/swaps/03-simulate-offchain.md | 31 ++++---- docs/sdk/v3/guides/swaps/04-routing.md | 20 +++--- 12 files changed, 152 insertions(+), 159 deletions(-) diff --git a/docs/sdk/v3/guides/02-local-development.md b/docs/sdk/v3/guides/02-local-development.md index fd1808604c..84958bff24 100644 --- a/docs/sdk/v3/guides/02-local-development.md +++ b/docs/sdk/v3/guides/02-local-development.md @@ -33,8 +33,8 @@ one of the network you want to use. For this guide, the following packages are used: -- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) -- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) +- [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) +- [`@uniswapfoundation/sdk-core`](https://www.npmjs.com/package/@uniswapfoundation/sdk-core) - [`ethers@5`](https://www.npmjs.com/package/ethers) Please note that we use ethers version 5, as this is still the most commonly used version of ethers.js. diff --git a/docs/sdk/v3/guides/advanced/03-price-oracle.md b/docs/sdk/v3/guides/advanced/03-price-oracle.md index b7c41201db..fe839eb365 100644 --- a/docs/sdk/v3/guides/advanced/03-price-oracle.md +++ b/docs/sdk/v3/guides/advanced/03-price-oracle.md @@ -26,7 +26,7 @@ Before diving into this guide, consider reading the theory behind using Uniswap For this guide, the following Uniswap packages are used: -- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) +- [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) The core code of this guide can be found in [`oracle.ts`](https://github.com/Uniswap/examples/tree/main/v3-sdk/price-oracle/src/libs/oracle.ts) @@ -40,12 +40,12 @@ const provider = new ethers.providers.JsonRpcProvider( '...rpcUrl' ) -const pool = Pool.initFromChain( +const pool = await Pool.initFromChain({ provider, - CurrentConfig.pool.token0, - CurrentConfig.pool.token1, - CurrentConfig.pool.fee -) + tokenA: CurrentConfig.pool.token0, + tokenB: CurrentConfig.pool.token1, + fee: CurrentConfig.pool.fee, + }) ``` All V3 pools store observations of the current tick and the block timestamp. @@ -86,10 +86,10 @@ import { ethers } from 'ethers' let wallet = new ethers.Wallet('private_key', provider) const observationCardinalityNext = 10 -const txRes = await pool.increaseObservationCardinalityNext( - wallet, - observationCardinalityNext -) +const txRes = await pool.rpcIncreaseObservationCardinalityNext({ + signer: wallet, + observationCardinalityNext, + }) ``` The Pool will now fill the open Observation Slots. @@ -121,7 +121,7 @@ function observe( The sdk wraps this function in the `rpcObserve` function on our Pool object. ```typescript - const observeResponse = await pool.rpcObserve(timestamps) + const observeResponse = await pool.rpcObserve({ secondsAgo: timestamps }) ``` Let's create an interface to map our data to: @@ -168,15 +168,13 @@ We cannot directly use the value of a single Observation for anything meaningful const diffTickCumulative = observations[0].tickCumulative - observations[1].tickCumulative const secondsBetween = 108 -const averageTick = diffTickCumulative / secondsBetween +const averageTick = Number(diffTickCumulative / BigInt(secondsBetween)) ``` -Now that we know the average active Tick over the last 10 blocks, we can calculate the price with the `tickToPrice` function, which returns a [`Price`](../../../core/reference/classes/Price.md) Object. Check out the [Pool data](./02-pool-data.md) guide to understand how to construct a Pool Object and access its properties. We don't need the full Tick Data for this guide. +Now that we know the average active Tick over the last 10 blocks, we can calculate the price with the `tickToPrice` function, which returns a [`Price`](../../../core/reference/classes/Price.md) Object. ```typescript -import { tickToPrice, Pool } from '@uniswap/v3-sdk' - -const pool = new Pool(...) +import { tickToPrice } from '@uniswapfoundation/v3-sdk' const TWAP = tickToPrice(pool.token0, pool.token1, averageTick) ``` diff --git a/docs/sdk/v3/guides/advanced/04-range-orders.md b/docs/sdk/v3/guides/advanced/04-range-orders.md index fc61b44c44..c12ce4fb7a 100644 --- a/docs/sdk/v3/guides/advanced/04-range-orders.md +++ b/docs/sdk/v3/guides/advanced/04-range-orders.md @@ -29,8 +29,8 @@ Before working through this guide, consider checking out the Range Orders [conce For this guide, the following Uniswap packages are used: -- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) -- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) +- [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) +- [`@uniswapfoundation/sdk-core`](https://www.npmjs.com/package/@uniswapfoundation/sdk-core) The core code of this guide can be found in [`range-order.ts`](https://github.com/Uniswap/examples/tree/main/v3-sdk/range-order/src/libs/range-order.ts). @@ -85,7 +85,7 @@ We create a Pool that represents the V3 Pool we are interacting with and get the We won't need full tick data in this example. ```typescript -import { Pool } from '@uniswap/v3-sdk' +import { Pool } from '@uniswapfoundation/v3-sdk' const pool = new Pool(token0, token1, fee, sqrtPriceX96, liquidity, tickCurrent) @@ -95,7 +95,7 @@ const currentPrice = pool.token0Price Next we increase the `Price` by 5%. We create a new Price with a numerator 5% higher than our current Price: ```typescript -import { Price, Fraction } from '@uniswap/sdk-core' +import { Price, Fraction } from '@uniswapfoundation/sdk-core' const targetFraction = Price.asFraction.multiply(new Fraction(100 + 5, 100)) @@ -119,7 +119,7 @@ We use the `priceToClosestTick` function to find the closest tick to our targetP We then use the `nearestUsableTick` function to find the closest initializable Tick for the `tickSpacing` of the `Pool`. ```typescript -import {priceToClosestTick, nearestUsableTick} from '@uniswap/v3-sdk' +import {priceToClosestTick, nearestUsableTick} from '@uniswapfoundation/v3-sdk' let targetTick = nearestUsableTick( priceToClosestTick(targetPrice), @@ -163,7 +163,7 @@ If you are not familiar with liquidity Positions, check out the [liquidity posit We create a `Position` object with our ticks and the amount of tokens we want to deposit: ```typescript -import { Position } from '@uniswap/v3-sdk' +import { Position } from '@uniswapfoundation/v3-sdk' const position = Position.fromAmount0({ pool: pool, @@ -201,7 +201,7 @@ Once we have our approval, we create the calldata for the **Mint** call using th ```typescript import {MintOptions, NonfungiblePositionManager} -import { Percent } from '@uniswap/sdk-core' +import { Percent } from '@uniswapfoundation/sdk-core' const mintOptions: MintOptions = { recipient: wallet.address, @@ -331,7 +331,7 @@ We check if the tick has crossed our position, and if so we withdraw the Positio We use the NonfungiblePositionManager together with the `tokenId` to get all info of our position as we may have gotten fees from trades on the Pool: ```typescript -import { NonfungiblePositionManager } from '@uniswap/v3-sdk' +import { NonfungiblePositionManager } from '@uniswapfoundation/v3-sdk' const currentPosition = await NonfungiblePositionManager.fetchWithPositionId(getProvider(), tokenId) ``` @@ -341,8 +341,8 @@ We use the `NonfungiblePositionManager`, the `pool`, `positionInfo` and `tokenId We start with creating `CollectOptions`: ```typescript -import { Percent, CurrencyAmount } from '@uniswap/sdk-core' -import { CollectOptions, RemoveLiquidityOptions } from '@uniswap/v3-sdk' +import { Percent, CurrencyAmount } from '@uniswapfoundation/sdk-core' +import { CollectOptions, RemoveLiquidityOptions } from '@uniswapfoundation/v3-sdk' const collectOptions: Omit = { expectedCurrencyOwed0: CurrencyAmount.fromRawAmount( diff --git a/docs/sdk/v3/guides/liquidity/02-minting-position.md b/docs/sdk/v3/guides/liquidity/02-minting-position.md index 77a6e871e7..fc5d50be15 100644 --- a/docs/sdk/v3/guides/liquidity/02-minting-position.md +++ b/docs/sdk/v3/guides/liquidity/02-minting-position.md @@ -13,7 +13,7 @@ To run this example, check out the examples's [README](https://github.com/Uniswa If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our [background](../01-background.md) page! ::: -In the Uniswap V3 protocol, liquidity positions are represented using non-fungible tokens. In this guide we will use the `NonfungiblePositionManager` class to help us mint a liquidity position for the **USDC - DAI** pair. The inputs to our guide are the **two tokens** that we are pooling for, the **amount** of each token we are pooling for and the Pool **fee**. +In the Uniswap V3 protocol, liquidity positions are represented using non-fungible tokens. In this guide we will mint a liquidity position for the **USDC - DAI** pair. The inputs to our guide are the **two tokens** that we are pooling for, the **amount** of each token we are pooling for and the Pool **fee**. The guide will **cover**: @@ -25,9 +25,8 @@ At the end of the guide, given the inputs above, we should be able to mint a liq For this guide, the following Uniswap packages are used: -- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) -- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) -- [`@uniswap/smart-order-router`](https://www.npmjs.com/package/@uniswap/smart-order-router) +- [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) +- [`@uniswapfoundation/sdk-core`](https://www.npmjs.com/package/@uniswapfoundation/sdk-core) The core code of this guide can be found in [`mintPosition()`](https://github.com/Uniswap/examples/blob/main/v3-sdk/minting-position/src/libs/positions.ts#L37) @@ -39,27 +38,25 @@ In situations where a smart contract is transfering tokens on our behalf, we nee We can use the `approveTokenTransfer()` function from the sdk for that: ```typescript -import { approveTokenTransfer } from '@uniswap/v3-sdk' -import { NONFUNGIBLE_POSITION_MANAGER_ADDRESSES } from '@uniswap/sdk-core' - -const signer = getWallet() +import { approveTokenTransfer } from '@uniswapfoundation/v3-sdk' +import { NONFUNGIBLE_POSITION_MANAGER_ADDRESSES } from '@uniswapfoundation/sdk-core' const positionManagerAddress = NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[ CurrentConfig.tokens.token0.chainId ] -const token0Approval = await approveTokenTransfer( - positionManagerAddress, - token0Address, - amount0, - signer -) -const token1Approval = await approveTokenTransfer( - positionManagerAddress, - token1Address, - amount1, - signer -) +const token0Approval = await approveTokenTransfer({ + contractAddress: positionManagerAddress, + tokenAddress: CurrentConfig.tokens.token0.address, + amount: amount0, + signer: getWallet(), + }) + const token1Approval = await approveTokenTransfer({ + contractAddress: positionManagerAddress, + tokenAddress: CurrentConfig.tokens.token1.address, + amount: amount1, + signer: getWallet(), + }) ``` We can get the Contract address for the NonfungiblePositionManager from the `NONFUNGIBLE_POSITION_MANAGER_ADDRESSES` in the sdk-core. @@ -69,23 +66,23 @@ We can get the Contract address for the NonfungiblePositionManager from the `NON To create our Position, we first need to instantiate a `Pool` object: ```typescript - import {Pool} from '@uniswap/v3-sdk' + import {Pool} from '@uniswapfoundation/v3-sdk' const provider = getProvider() - const pool = await Pool.initFromChain( + const pool = await Pool.initFromChain({ provider, - CurrentConfig.tokens.in, - CurrentConfig.tokens.out, - CurrentConfig.tokens.poolFee - ) + tokenA: CurrentConfig.tokens.token0, + tokenB: CurrentConfig.tokens.token1, + fee: CurrentConfig.tokens.poolFee, + }) ``` Next, we can use the pool to create an instance of a `Position` object, which represents a Liquidity Position offchain: ```typescript -import { Position } from '@uniswap/v3-sdk' -import { BigIntish } from '@uniswap/sdk-core' +import { Position } from '@uniswapfoundation/v3-sdk' +import { BigIntish } from '@uniswapfoundation/sdk-core' // The maximum token amounts we want to provide. BigIntish accepts number, string or bigint const amount0: BigIntish = ... @@ -114,11 +111,11 @@ Given those parameters, `fromAmounts` will attempt to calculate the maximum amou ## Configuring and executing our minting transaction -We can now use the `NonfungiblePositionManager` class to mint our Position: +We can now mint our Position: ```typescript -import { MintOptions, NonfungiblePositionManager } from '@uniswap/v3-sdk' -import { Percent } from '@uniswap/sdk-core' +import { MintOptions } from '@uniswapfoundation/v3-sdk' +import { Percent } from '@uniswapfoundation/sdk-core' const signer = getWallet() @@ -129,12 +126,11 @@ const mintOptions: MintOptions = { } // get calldata for minting a position -const txResponse = NonfungiblePositionManager.mintOnChain( - signer, - provider, - position, - mintOptions -) +const txResponse = await positionToMint.mint({ + signer: getWallet(), + provider, + options: mintOptions, + }) ``` The `MintOptions` interface requires three keys: @@ -143,7 +139,7 @@ The `MintOptions` interface requires three keys: - `deadline` defines the latest point in time at which we want our transaction to be included in the blockchain. - `slippageTolerance` defines the maximum amount of **change of the ratio** of the Tokens we provide. The ratio can change if for example **trades** that change the price of the Pool are included before our transaction. -The `mitOnChain()` function directly signs and executes a transaction that will create our Position. +The `mint()` function directly signs and executes a transaction that will create our Position. The effect of the transaction is to mint a new Position NFT. We should see a new position with liquidity in our list of positions. diff --git a/docs/sdk/v3/guides/liquidity/03-fetching-positions.md b/docs/sdk/v3/guides/liquidity/03-fetching-positions.md index 41998917a5..c1da8334f2 100644 --- a/docs/sdk/v3/guides/liquidity/03-fetching-positions.md +++ b/docs/sdk/v3/guides/liquidity/03-fetching-positions.md @@ -22,8 +22,8 @@ The guide will **cover**: For this guide, the following Uniswap packages are used: -- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) -- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) +- [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) +- [`@uniswapfoundation/sdk-core`](https://www.npmjs.com/package/@uniswapfoundation/sdk-core) ## Fetching the number of Positions @@ -32,7 +32,7 @@ We first fetch the number of positions an address owns using the `getPositionCou ```typescript import ethers from 'ethers' -import { Position } from '@uniswap/v3-sdk' +import { Position } from '@uniswapfoundation/v3-sdk' const provider = new ethers.providers.JsonRpcProvider('...rpcUrl') diff --git a/docs/sdk/v3/guides/liquidity/04-modifying-position.md b/docs/sdk/v3/guides/liquidity/04-modifying-position.md index ae1ec0945b..235c983ff7 100644 --- a/docs/sdk/v3/guides/liquidity/04-modifying-position.md +++ b/docs/sdk/v3/guides/liquidity/04-modifying-position.md @@ -22,8 +22,8 @@ At the end of the guide, given the inputs above, we should be able to add or rem For this guide, the following Uniswap packages are used: -- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) -- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) +- [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) +- [`@uniswapfoundation/sdk-core`](https://www.npmjs.com/package/@uniswapfoundation/sdk-core) The core code of this guide can be found in [`addLiquidity()`](https://github.com/Uniswap/examples/blob/d34a53412dbf905802da2249391788a225719bb8/v3-sdk/modifying-position/src/example/Example.tsx#L33) and [`removeLiquidity()`](https://github.com/Uniswap/examples/blob/733d586070afe2c8cceb35d557a77eac7a19a656/v3-sdk/modifying-position/src/example/Example.tsx#L83) @@ -77,7 +77,7 @@ Use the following snippet to fetch a position using the position id: ```typescript import { ethers } from 'ethers' -import { Position } from '@uniswap/v3-sdk' +import { Position } from '@uniswapfoundation/v3-sdk' // You need to know this, or fetch the position differently const myPositionId = "0xabcdef" @@ -98,7 +98,7 @@ The snippet to increase liquidity by 10% can be seen below: ```typescript import { ethers } from 'ethers' -import { Fraction } from '@uniswap/sdk-core' +import { Fraction } from '@uniswapfoundation/sdk-core' // Use your private key or another way to initialize a Wallet. // A different way would be to use the Metamask signer in the browser. @@ -179,7 +179,7 @@ Use the following snippet to fetch a position using the position id: ```typescript import { ethers } from 'ethers' -import { Position } from '@uniswap/v3-sdk' +import { Position } from '@uniswapfoundation/v3-sdk' // You need to know this, or fetch the position differently const myPositionId = "0xabcdef" @@ -201,7 +201,7 @@ The snippet to decrease liquidity by 10% can be seen below: ```typescript import { ethers } from 'ethers' -import { Fraction } from '@uniswap/sdk-core' +import { Fraction } from '@uniswapfoundation/sdk-core' // Use your private key or another way to initialize a Wallet. // A different way would be to use the Metamask signer in the browser. diff --git a/docs/sdk/v3/guides/liquidity/05-collecting-fees.md b/docs/sdk/v3/guides/liquidity/05-collecting-fees.md index 2ccb59d2e6..a217165d22 100644 --- a/docs/sdk/v3/guides/liquidity/05-collecting-fees.md +++ b/docs/sdk/v3/guides/liquidity/05-collecting-fees.md @@ -21,7 +21,7 @@ At the end of the guide, given the inputs above, we should be able to collect th For this guide, the following Uniswap packages are used: -- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) +- [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) - [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) The core code of this guide can be found in [`collectFees()`](https://github.com/Uniswap/examples/blob/main/v3-sdk/collecting-fees/src/libs/liquidity.ts#L35). @@ -39,7 +39,7 @@ All of the fee collecting logic can be found in the [`collectFees`](https://gith To start, we fetch the position from the NonfungiblePositionManager Contract to get the fees we are owed: ```typescript -import { Position } from '@uniswap/v3-sdk' +import { Position } from '@uniswapfoundation/v3-sdk' const position = await Position.fetchWithPositionId(provider, positionId) ``` diff --git a/docs/sdk/v3/guides/liquidity/06-swap-and-add-liquidity.md b/docs/sdk/v3/guides/liquidity/06-swap-and-add-liquidity.md index 96b12e6d21..15daa6c5a2 100644 --- a/docs/sdk/v3/guides/liquidity/06-swap-and-add-liquidity.md +++ b/docs/sdk/v3/guides/liquidity/06-swap-and-add-liquidity.md @@ -26,9 +26,9 @@ At the end of the guide, given the inputs above, we should be able swap-and-add For this guide, the following Uniswap packages are used: -- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) -- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) -- [`@uniswap/smart-order-router`](https://www.npmjs.com/package/@uniswap/smart-order-router) +- [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) +- [`@uniswapfoundation/sdk-core`](https://www.npmjs.com/package/@uniswapfoundation/sdk-core) +- [`@uniswapfoundation/smart-order-router`](https://www.npmjs.com/package/@uniswapfoundation/smart-order-router) The core code of this guide can be found in [`swapAndAddLiquidity()`](https://github.com/Uniswap/examples/blob/main/v3-sdk/swap-and-add-liquidity/src/libs/liquidity.ts#L48). @@ -44,27 +44,29 @@ The first step is to approve the `SwapRouter` smart contract to spend our tokens ```typescript // Give approval to the router contract to transfer tokens - const tokenInApproval = await getTokenTransferApproval( - V3_SWAP_ROUTER_ADDRESS, - CurrentConfig.tokens.token0, - TOKEN_AMOUNT_TO_APPROVE_FOR_TRANSFER - ) - - const tokenOutApproval = await getTokenTransferApproval( - V3_SWAP_ROUTER_ADDRESS, - CurrentConfig.tokens.token1, - TOKEN_AMOUNT_TO_APPROVE_FOR_TRANSFER - ) + const tokenInApproval = await approveTokenTransfer({ + contractAddress: V3_SWAP_ROUTER_ADDRESS, + tokenAddress: CurrentConfig.tokens.token0.address, + amount: TOKEN_AMOUNT_TO_APPROVE_FOR_TRANSFER, + signer: getWallet(), + }) + + const tokenOutApproval = await approveTokenTransfer({ + contractAddress: V3_SWAP_ROUTER_ADDRESS, + tokenAddress: CurrentConfig.tokens.token1.address, + amount: TOKEN_AMOUNT_TO_APPROVE_FOR_TRANSFER, + signer: getWallet(), + }) ``` We described the `getTokenTransferApproval` function [here](./02-minting-position.md#giving-approval-to-transfer-our-tokens). We defined the address for the swaprouter in our `constants.ts` file. -Then we can setup our router, the [`AlphaRouter`](https://github.com/Uniswap/smart-order-router/blob/97c1bb7cb64b22ebf3509acda8de60c0445cf250/src/routers/alpha-router/alpha-router.ts#L333), which is part of the [smart-order-router package](https://www.npmjs.com/package/@uniswap/smart-order-router). The router requires a `chainId` and a `provider` to be initialized. Note that routing is not supported for local forks, so we will use a mainnet provider even when swapping on a local fork: +Then we can setup our router, the [`AlphaRouter`](https://github.com/Uniswap/smart-order-router/blob/97c1bb7cb64b22ebf3509acda8de60c0445cf250/src/routers/alpha-router/alpha-router.ts#L333), which is part of the [smart-order-router package](https://www.npmjs.com/package/@uniswapfoundation/smart-order-router). The router requires a `chainId` and a `provider` to be initialized. Note that routing is not supported for local forks, so we will use a mainnet provider even when swapping on a local fork: ```typescript import { ethers } from 'ethers' -import { AlphaRouter } from '@uniswap/smart-order-router' +import { AlphaRouter } from '@uniswapfoundation/smart-order-router' const provider = new ethers.providers.JsonRpcProvider(rpcUrl) @@ -80,7 +82,7 @@ Having created the router, we now need to construct the parameters required to m The first two parameters are the currency amounts we use as input to the `routeToRatio` algorithm: ```typescript -import { CurrencyAmount } from '@uniswap/sdk-core' +import { CurrencyAmount } from '@uniswapfoundation/sdk-core' const token0CurrencyAmount = CurrencyAmount.fromRawAmount( token0, @@ -102,7 +104,7 @@ const token1CurrencyAmount = CurrencyAmount.fromRawAmount( Next, we will create a placeholder position with a liquidity of `1` since liquidity is still unknown and will be set inside the call to `routeToRatio`: ```typescript -import { Pool, Position, nearestUsableTick } from '@uniswap/v3-sdk' +import { Pool, Position, nearestUsableTick } from '@uniswapfoundation/v3-sdk' const placeholderPosition = new Position{ pool, @@ -122,8 +124,8 @@ We then need to create an instance of `SwapAndAddConfig` which will set addition - `maxIterations` determines the maximum times the algorithm will iterate to find a ratio within error tolerance. If max iterations is exceeded, an error is returned. The benefit of running the algorithm more times is that we have more chances to find a route, but more iterations will longer to execute. We've used a default of 6 in our example. ```typescript -import { Fraction } from '@uniswap/sdk-core' -import { SwapAndAddConfig } from '@uniswap/smart-order-router' +import { Fraction } from '@uniswapfoundation/sdk-core' +import { SwapAndAddConfig } from '@uniswapfoundation/smart-order-router' const swapAndAddConfig: SwapAndAddConfig = { ratioErrorTolerance: new Fraction(1, 100), @@ -137,7 +139,7 @@ Finally, we will create an instance of `SwapAndAddOptions` to configure which po - **`addLiquidityOptions`** must contain a `tokenId` to add to an existing position ```typescript -import { SwapAndAddOptions } from '@uniswap/smart-order-router' +import { SwapAndAddOptions } from '@uniswapfoundation/smart-order-router' const swapAndAddOptions: SwapAndAddOptions = { swapOptions: { @@ -157,7 +159,7 @@ const swapAndAddOptions: SwapAndAddOptions = { Having constructed all the parameters we need to call `routeToRatio`, we can now make the call to the function: ```typescript -import { SwapToRatioResponse } from '@uniswap/smart-order-router' +import { SwapToRatioResponse } from '@uniswapfoundation/smart-order-router' const routeToRatioResponse: SwapToRatioResponse = await router.routeToRatio( token0CurrencyAmount, @@ -171,7 +173,7 @@ const routeToRatioResponse: SwapToRatioResponse = await router.routeToRatio( The return type of the function call is [SwapToRatioResponse](https://github.com/Uniswap/smart-order-router/blob/97c1bb7cb64b22ebf3509acda8de60c0445cf250/src/routers/router.ts#L121). If a route was found successfully, this object will have two fields: the status (success) and the `SwapToRatioRoute` object. We check to make sure that both of those conditions hold true before we construct and submit the transaction: ```typescript -import { SwapToRatioStatus } from '@uniswap/smart-order-router' +import { SwapToRatioStatus } from '@uniswapfoundation/smart-order-router' if ( !routeToRatioResponse || @@ -188,7 +190,7 @@ In case a route was not found, we return from the function a `Failed` state for After making sure that a route was successfully found, we can now construct and send the transaction. The response (`SwapToRatioRoute`) will have the properties we need to construct our transaction object: ```typescript -import { SwapToRatioRoute } from '@uniswap/smart-order-router' +import { SwapToRatioRoute } from '@uniswapfoundation/smart-order-router' const route: SwapToRatioRoute = routeToRatioResponse.result const transaction = { diff --git a/docs/sdk/v3/guides/swaps/01-quoting.md b/docs/sdk/v3/guides/swaps/01-quoting.md index 2b6a814b59..7aa8f6df15 100644 --- a/docs/sdk/v3/guides/swaps/01-quoting.md +++ b/docs/sdk/v3/guides/swaps/01-quoting.md @@ -24,8 +24,8 @@ At the end of the guide, we should be able to fetch a quote for the given input For this guide, the following Uniswap packages are used: -- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) -- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) +- [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) +- [`@uniswapfoundation/sdk-core`](https://www.npmjs.com/package/@uniswapfoundation/sdk-core) The core code of this guide can be found in [`quote.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/quoting/src/libs/quote.ts) @@ -34,8 +34,8 @@ The core code of this guide can be found in [`quote.ts`](https://github.com/Unis We will use the example configuration `CurrentConfig` in most code snippets of this guide. It has the format: ```typescript -import { Token } from '@uniswap/sdk-core' -import { FeeAmount } from '@uniswap/v3-sdk' +import { Token } from '@uniswapfoundation/sdk-core' +import { FeeAmount } from '@uniswapfoundation/v3-sdk' interface ExampleConfig { rpc: { @@ -114,16 +114,16 @@ const currencyAmountIn = CurrencyAmount.fromRawAmount( We can now use the SwapQuoter class to fetch a quote for our swap. We need a provider to connect to the blockchain: ```typescript -import { SwapQuoter } from '@uniswap/v3-sdk' +import { SwapQuoter } from '@uniswapfoundation/v3-sdk' const provider = new ethers.providers.JsonRpcProvider(CurrentConfig.rpc.mainnet) -const currencyAmountOut = await SwapQuoter.quoteExactInputSingle( - currencyAmountInt, - CurrentConfig.tokens.out, - CurrentConfig.tokens.poolFee, - provider -) +const currencyAmountOut = await SwapQuoter.quoteExactInputSingle({ + amountIn: currencyAmountIn, + tokenOut: CurrentConfig.tokens.out, + poolFee: CurrentConfig.tokens.poolFee, + provider, + }) ``` The function calls the Quoter contract and parses the response value. It only works on chains where Uniswap has officially deployed the QuoterV2 contract. diff --git a/docs/sdk/v3/guides/swaps/02-trading.md b/docs/sdk/v3/guides/swaps/02-trading.md index 1020d6fa4a..47b0868ecc 100644 --- a/docs/sdk/v3/guides/swaps/02-trading.md +++ b/docs/sdk/v3/guides/swaps/02-trading.md @@ -29,8 +29,8 @@ Included in the example application is functionality to wrap/unwrap ETH as neede For this guide, the following Uniswap packages are used: -- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) -- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) +- [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) +- [`@uniswapfoundation/sdk-core`](https://www.npmjs.com/package/@uniswapfoundation/sdk-core) The core code of this guide can be found in [`trading.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/trading/src/libs/trading.ts) @@ -112,7 +112,7 @@ To construct our trade, we will first create a model instance of a `Pool`. We cr The sdk has a utility function to create a Pool from onchain data: ```typescript - import {Pool} from '@uniswap/v3-sdk' + import {Pool} from '@uniswapfoundation/v3-sdk' import ethers from 'ethers' const provider = new ethers.providers.JsonRpcProvider(CurrentConfig.rpc.mainnet) @@ -153,7 +153,7 @@ The `Route` object can find this route from an array of given pools and an input To keep it simple for this guide, we only swap over one Pool: ```typescript -import { Route } from '@uniswap/v3-sdk' +import { Route } from '@uniswapfoundation/v3-sdk' const swapRoute = new Route( [pool], @@ -176,8 +176,8 @@ As shown below, the quote is obtained using the `v3-sdk`'s `SwapQuoter`, for thi In contrast to the `quoteExactInputSingle()` function we used in the previous guide, this function works for a Route with any number of Uniswap V3 Pools, not just a swap over a single Pool: ```typescript -import { SwapQuoter } from '@uniswap/v3-sdk' -import { CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { SwapQuoter } from '@uniswapfoundation/v3-sdk' +import { CurrencyAmount, TradeType } from '@uniswapfoundation/sdk-core' const rawInputAmount = ethers.utils.parseUnits( CurrentConfig.tokens.amountIn, @@ -189,12 +189,12 @@ const currencyAmountIn = CurrencyAmount.fromRawAmount( rawInputAmount ) -const expectedOutput = await SwapQuoter.callQuoter( - swapRoute, - currencyAmountIn, - TradeType.EXACT_INPUT, +const expectedOutput = await SwapQuoter.callQuoter({ + route: swapRoute, + amount: currencyAmountIn, + tradeType: TradeType.EXACT_INPUT, provider -) +}) ``` We construct the input the same way we did in the previous guide. @@ -204,7 +204,7 @@ With the quote and the route, we can now construct a trade using the route in ad Because we already know the expected output of our Trade, we do not have to check it again. We can use the `uncheckedTrade` function to create our Trade: ```typescript -import { Trade } from 'uniswap/v3-sdk' +import { Trade } from '@uniswapfoundation/v3-sdk' const uncheckedTrade = Trade.createUncheckedTrade({ route: swapRoute, @@ -223,8 +223,8 @@ We will use the `executeTrade()` function of the `SwapRouter` class. First we specify the deadline and the slippage tolerance we are willing to accept for our trade: ```typescript -import { SwapOptions } frpm '@uniswap/v3-sdk' -import { Percent } from '@uniswap/sdk-core' +import { SwapOptions } frpm '@uniswapfoundation/v3-sdk' +import { Percent } from '@uniswapfoundation/sdk-core' const swapOptions: SwapOptions = { slippageTolerance: new Percent(50, 10_000), @@ -250,13 +250,13 @@ wallet.connect(provider) We are now ready to execute our trade: ```typescript -import { SwapRouter } from '@uniswap/v3-sdk' +import { SwapRouter } from '@uniswapfoundation/v3-sdk' -const txResponse = await SwapRouter.executeTrade( - [uncheckedTrade], - swapOptions, - wallet -) +const txResponse = await SwapRouter.executeTrade({ + trades: [uncheckedTrade], + options: swapOptions, + signer: wallet +}) ``` The function automatically checks if the necessary token transfer approvals exist and creates them if not. diff --git a/docs/sdk/v3/guides/swaps/03-simulate-offchain.md b/docs/sdk/v3/guides/swaps/03-simulate-offchain.md index c7458cf82c..003bbcf38d 100644 --- a/docs/sdk/v3/guides/swaps/03-simulate-offchain.md +++ b/docs/sdk/v3/guides/swaps/03-simulate-offchain.md @@ -25,8 +25,8 @@ At the end of the guide, we should be able to initialize Pools with full tickdat For this guide, the following Uniswap packages are used: -- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) -- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) +- [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) +- [`@uniswapfoundation/sdk-core`](https://www.npmjs.com/package/@uniswapfoundation/sdk-core) The core code of this guide can be found in [`simulations.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/offchain-simulation/src/libs/simulations.ts) @@ -35,8 +35,8 @@ The core code of this guide can be found in [`simulations.ts`](https://github.co We will use the example configuration `CurrentConfig` in most code snippets of this guide. It has the format: ```typescript -import { Token } from '@uniswap/sdk-core' -import { FeeAmount, Pool } from '@uniswap/v3-sdk' +import { Token } from '@uniswapfoundation/sdk-core' +import { FeeAmount, Pool } from '@uniswapfoundation/v3-sdk' interface ExampleConfig { rpc: { @@ -114,8 +114,8 @@ In this example, we want to make an exact Input trade from **USDC** to **DAI**. We use the `Trade.bestTradeExactIn` function to find the best route given our Pools: ```typescript -import { Trade, BestTradeOptions } from '@uniswap/v3-sdk' -import { CurrencyAmount } from '@uniswap/sdk-core' +import { Trade, BestTradeOptions } from '@uniswapfoundation/v3-sdk' +import { CurrencyAmount } from '@uniswapfoundation/sdk-core' const currencyAmountIn = CurrencyAmount.fromRawAmount(...) @@ -145,13 +145,12 @@ We can access them with the `swaps` getter if we want to access this information Like in the previous guide, we execute the trade with `executeTrade()` on the `SwapRouter` class: ```typescript -import { SwapRouter } from '@uniswap/v3-sdk' +import { SwapRouter } from '@uniswapfoundation/v3-sdk' wallet.connect(provider) const txResponse = SwapRouter.executeTrade( - [bestTrade[0]], - , - wallet + trades: [bestTrade[0]], + signer: wallet ) ``` @@ -164,13 +163,11 @@ wallet.connect(provider) const txResponse = SwapRouter.executeBestSimulatedSwapOnPools( pools, - currencyAmountIn, - CurrentConfig.tokens.in, - CurrentConfig.tokens.out, - TradeType.EXACT_INPUT, - undefined, - undefined, - wallet + amount: currencyAmountIn, + currencyIn: CurrentConfig.tokens.in, + currencyOut: CurrentConfig.tokens.out, + tradeType: TradeType.EXACT_INPUT, + signer: wallet ) ``` diff --git a/docs/sdk/v3/guides/swaps/04-routing.md b/docs/sdk/v3/guides/swaps/04-routing.md index 0ae265bd98..3c3c775084 100644 --- a/docs/sdk/v3/guides/swaps/04-routing.md +++ b/docs/sdk/v3/guides/swaps/04-routing.md @@ -23,16 +23,16 @@ At the end of the guide, we should be able to create a route and and execute a s For this guide, the following Uniswap packages are used: -- [`@uniswap/v3-sdk`](https://www.npmjs.com/package/@uniswap/v3-sdk) -- [`@uniswap/sdk-core`](https://www.npmjs.com/package/@uniswap/sdk-core) -- [`@uniswap/smart-order-router`](https://www.npmjs.com/package/@uniswap/smart-order-router) +- [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) +- [`@uniswapfoundation/sdk-core`](https://www.npmjs.com/package/@uniswapfoundation/sdk-core) +- [`@uniswapfoundation/smart-order-router`](https://www.npmjs.com/package/@uniswapfoundation/smart-order-router) The core code of this guide can be found in [`routing.ts`](https://github.com/Uniswap/examples/blob/main/v3-sdk/routing/src/libs/routing.ts) The config, which we will use in some code snippets in this guides has this structure: ```typescript -import { Token } from '@uniswap/sdk-core' +import { Token } from '@uniswapfoundation/sdk-core' interface ExampleConfig { env: Environment @@ -56,10 +56,10 @@ export const CurrentConfig: ExampleConfig = {...} ## Creating a router instance -To compute our route, we will use the `@uniswap/smart-order-router` package, specifically the `AlphaRouter` class which requires a `chainId` and a `provider`. Note that routing is not supported for local forks, so we will use a mainnet provider even when swapping on a local fork: +To compute our route, we will use the `@uniswapfoundation/smart-order-router` package, specifically the `AlphaRouter` class which requires a `chainId` and a `provider`. Note that routing is not supported for local forks, so we will use a mainnet provider even when swapping on a local fork: ```typescript -import { AlphaRouter, ChainId } from '@uniswap/smart-order-router' +import { AlphaRouter, ChainId } from '@uniswapfoundation/smart-order-router' const provider = new ethers.providers.JsonRpcProvider(rpcUrl) @@ -77,8 +77,8 @@ In contrast to the contract we used previously, this on can execute swaps on bot The `smart-order-router` package provides us with a `SwapOptionsSwapRouter02` interface, defining the wallet to use, slippage tolerance, and deadline for the transaction that we need to interact with the contract: ```typescript -import { SwapOptionsSwapRouter02, SwapType } from '@uniswap/smart-order-router' -import { Percent } from '@uniswap/sdk-core' +import { SwapOptionsSwapRouter02, SwapType } from '@uniswapfoundation/smart-order-router' +import { Percent } from '@uniswapfoundation/sdk-core' const options: SwapOptionsSwapRouter02 = { recipient: CurrentConfig.wallet.address, @@ -93,9 +93,9 @@ Like explained in the [trading guide](./02-trading.md#executing-a-trade), it is Using these options, we can now create a trade (`TradeType.EXACT_INPUT` or `TradeType.EXACT_OUTPUT`) with the currency and the input amount to use to get a quote. For this example, we'll use an `EXACT_INPUT` trade to get a quote outputted in the quote currency. ```typescript -import { CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { CurrencyAmount, TradeType } from '@uniswapfoundation/sdk-core' -const rawTokenAmountIn: JSBI = fromReadableAmount( +const rawTokenAmountIn = fromReadableAmount( CurrentConfig.currencies.amountIn, CurrentConfig.currencies.in.decimals ) From e90b624caf861f6839f8f01a31c5057971b1e1e0 Mon Sep 17 00:00:00 2001 From: Florian Winkler Date: Sat, 6 Jan 2024 19:00:26 +0100 Subject: [PATCH 52/52] V3-sdk guides sdk publisher disclaimer --- docs/sdk/v3/guides/advanced/03-price-oracle.md | 6 ++++++ docs/sdk/v3/guides/advanced/04-range-orders.md | 6 ++++++ docs/sdk/v3/guides/liquidity/02-minting-position.md | 6 ++++++ docs/sdk/v3/guides/liquidity/03-fetching-positions.md | 6 ++++++ docs/sdk/v3/guides/liquidity/04-modifying-position.md | 6 ++++++ docs/sdk/v3/guides/liquidity/05-collecting-fees.md | 6 ++++++ docs/sdk/v3/guides/liquidity/06-swap-and-add-liquidity.md | 6 ++++++ docs/sdk/v3/guides/swaps/01-quoting.md | 8 +++++++- docs/sdk/v3/guides/swaps/02-trading.md | 6 ++++++ docs/sdk/v3/guides/swaps/03-simulate-offchain.md | 6 ++++++ docs/sdk/v3/guides/swaps/04-routing.md | 6 ++++++ 11 files changed, 67 insertions(+), 1 deletion(-) diff --git a/docs/sdk/v3/guides/advanced/03-price-oracle.md b/docs/sdk/v3/guides/advanced/03-price-oracle.md index fe839eb365..b2957f7430 100644 --- a/docs/sdk/v3/guides/advanced/03-price-oracle.md +++ b/docs/sdk/v3/guides/advanced/03-price-oracle.md @@ -24,6 +24,12 @@ This guide will **cover**: Before diving into this guide, consider reading the theory behind using Uniswap V3 as an [Onchain Oracle](../../../../concepts/protocol/oracle.md). +:::info +The SDKs that are used in the guide are now published by the [Uniswap Foundation](https://github.com/uniswapfoundation) instead of Uniswap Labs. +You can find a list of supported SDKs [here](https://www.npmjs.com/org/uniswapfoundation). +Make sure you don't mix SDKs published by Uniswap Labs and the Uniswap Foundation to avoid unpredictable behavior. +::: + For this guide, the following Uniswap packages are used: - [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) diff --git a/docs/sdk/v3/guides/advanced/04-range-orders.md b/docs/sdk/v3/guides/advanced/04-range-orders.md index c12ce4fb7a..1d010f83ee 100644 --- a/docs/sdk/v3/guides/advanced/04-range-orders.md +++ b/docs/sdk/v3/guides/advanced/04-range-orders.md @@ -27,6 +27,12 @@ This guide will **cover**: Before working through this guide, consider checking out the Range Orders [concept page](../../../../concepts/protocol/range-orders.md) to understand how Limit orders can be executed with Uniswap V3. +:::info +The SDKs that are used in the guide are now published by the [Uniswap Foundation](https://github.com/uniswapfoundation) instead of Uniswap Labs. +You can find a list of supported SDKs [here](https://www.npmjs.com/org/uniswapfoundation). +Make sure you don't mix SDKs published by Uniswap Labs and the Uniswap Foundation to avoid unpredictable behavior. +::: + For this guide, the following Uniswap packages are used: - [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) diff --git a/docs/sdk/v3/guides/liquidity/02-minting-position.md b/docs/sdk/v3/guides/liquidity/02-minting-position.md index fc5d50be15..0672d1f8d2 100644 --- a/docs/sdk/v3/guides/liquidity/02-minting-position.md +++ b/docs/sdk/v3/guides/liquidity/02-minting-position.md @@ -23,6 +23,12 @@ The guide will **cover**: At the end of the guide, given the inputs above, we should be able to mint a liquidity position with the press of a button and view the position on the UI of the web application. +:::info +The SDKs that are used in the guide are now published by the [Uniswap Foundation](https://github.com/uniswapfoundation) instead of Uniswap Labs. +You can find a list of supported SDKs [here](https://www.npmjs.com/org/uniswapfoundation). +Make sure you don't mix SDKs published by Uniswap Labs and the Uniswap Foundation to avoid unpredictable behavior. +::: + For this guide, the following Uniswap packages are used: - [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) diff --git a/docs/sdk/v3/guides/liquidity/03-fetching-positions.md b/docs/sdk/v3/guides/liquidity/03-fetching-positions.md index c1da8334f2..4b1242375e 100644 --- a/docs/sdk/v3/guides/liquidity/03-fetching-positions.md +++ b/docs/sdk/v3/guides/liquidity/03-fetching-positions.md @@ -20,6 +20,12 @@ The guide will **cover**: 1. Fetching all positions for an address. 2. Fetching the position info for the positions. +:::info +The SDKs that are used in the guide are now published by the [Uniswap Foundation](https://github.com/uniswapfoundation) instead of Uniswap Labs. +You can find a list of supported SDKs [here](https://www.npmjs.com/org/uniswapfoundation). +Make sure you don't mix SDKs published by Uniswap Labs and the Uniswap Foundation to avoid unpredictable behavior. +::: + For this guide, the following Uniswap packages are used: - [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) diff --git a/docs/sdk/v3/guides/liquidity/04-modifying-position.md b/docs/sdk/v3/guides/liquidity/04-modifying-position.md index 235c983ff7..2e980cffe2 100644 --- a/docs/sdk/v3/guides/liquidity/04-modifying-position.md +++ b/docs/sdk/v3/guides/liquidity/04-modifying-position.md @@ -20,6 +20,12 @@ The guide will **cover**: At the end of the guide, given the inputs above, we should be able to add or remove liquidity from a minted position with the press of a button and see the change reflected in our position and the balance of our tokens. +:::info +The SDKs that are used in the guide are now published by the [Uniswap Foundation](https://github.com/uniswapfoundation) instead of Uniswap Labs. +You can find a list of supported SDKs [here](https://www.npmjs.com/org/uniswapfoundation). +Make sure you don't mix SDKs published by Uniswap Labs and the Uniswap Foundation to avoid unpredictable behavior. +::: + For this guide, the following Uniswap packages are used: - [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) diff --git a/docs/sdk/v3/guides/liquidity/05-collecting-fees.md b/docs/sdk/v3/guides/liquidity/05-collecting-fees.md index a217165d22..a070af76c2 100644 --- a/docs/sdk/v3/guides/liquidity/05-collecting-fees.md +++ b/docs/sdk/v3/guides/liquidity/05-collecting-fees.md @@ -19,6 +19,12 @@ The guide will **cover**: At the end of the guide, given the inputs above, we should be able to collect the accrued fees (if any) of a minted position with the press of a button and see the change reflected in our position and the balance of our tokens. +:::info +The SDKs that are used in the guide are now published by the [Uniswap Foundation](https://github.com/uniswapfoundation) instead of Uniswap Labs. +You can find a list of supported SDKs [here](https://www.npmjs.com/org/uniswapfoundation). +Make sure you don't mix SDKs published by Uniswap Labs and the Uniswap Foundation to avoid unpredictable behavior. +::: + For this guide, the following Uniswap packages are used: - [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) diff --git a/docs/sdk/v3/guides/liquidity/06-swap-and-add-liquidity.md b/docs/sdk/v3/guides/liquidity/06-swap-and-add-liquidity.md index 15daa6c5a2..d47e25c119 100644 --- a/docs/sdk/v3/guides/liquidity/06-swap-and-add-liquidity.md +++ b/docs/sdk/v3/guides/liquidity/06-swap-and-add-liquidity.md @@ -24,6 +24,12 @@ The guide will **cover**: At the end of the guide, given the inputs above, we should be able swap-and-add liquidity using 100% of the input assets with the press of a button and see the change reflected in our position and the balance of our tokens. +:::info +The SDKs that are used in the guide are now published by the [Uniswap Foundation](https://github.com/uniswapfoundation) instead of Uniswap Labs. +You can find a list of supported SDKs [here](https://www.npmjs.com/org/uniswapfoundation). +Make sure you don't mix SDKs published by Uniswap Labs and the Uniswap Foundation to avoid unpredictable behavior. +::: + For this guide, the following Uniswap packages are used: - [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) diff --git a/docs/sdk/v3/guides/swaps/01-quoting.md b/docs/sdk/v3/guides/swaps/01-quoting.md index 7aa8f6df15..10778b33e7 100644 --- a/docs/sdk/v3/guides/swaps/01-quoting.md +++ b/docs/sdk/v3/guides/swaps/01-quoting.md @@ -22,6 +22,12 @@ The guide will **cover**: At the end of the guide, we should be able to fetch a quote for the given input token pair and the input token amount with the press of a button on the web application. +:::info +The SDKs that are used in the guide are now published by the [Uniswap Foundation](https://github.com/uniswapfoundation) instead of Uniswap Labs. +You can find a list of supported SDKs [here](https://www.npmjs.com/org/uniswapfoundation). +Make sure you don't mix SDKs published by Uniswap Labs and the Uniswap Foundation to avoid unpredictable behavior. +::: + For this guide, the following Uniswap packages are used: - [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) @@ -98,7 +104,7 @@ The function expects a `CurrencyAmount` object. We use ethers to parse the input ```typescript import { ethers } from 'ethers' -import { CurrencyAmount } from 'sdk-core' +import { CurrencyAmount } from '@uniswapfoundation/sdk-core' const rawInputAmount = ethers.utils.parseUnits( CurrentConfig.tokens.amountIn, diff --git a/docs/sdk/v3/guides/swaps/02-trading.md b/docs/sdk/v3/guides/swaps/02-trading.md index 47b0868ecc..1fa6699dce 100644 --- a/docs/sdk/v3/guides/swaps/02-trading.md +++ b/docs/sdk/v3/guides/swaps/02-trading.md @@ -27,6 +27,12 @@ At the end of the guide, we should be able to create and execute a trade between Included in the example application is functionality to wrap/unwrap ETH as needed to fund the example `WETH` to `USDC` swap directly from an `ETH` balance. ::: +:::info +The SDKs that are used in the guide are now published by the [Uniswap Foundation](https://github.com/uniswapfoundation) instead of Uniswap Labs. +You can find a list of supported SDKs [here](https://www.npmjs.com/org/uniswapfoundation). +Make sure you don't mix SDKs published by Uniswap Labs and the Uniswap Foundation to avoid unpredictable behavior. +::: + For this guide, the following Uniswap packages are used: - [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) diff --git a/docs/sdk/v3/guides/swaps/03-simulate-offchain.md b/docs/sdk/v3/guides/swaps/03-simulate-offchain.md index 003bbcf38d..738dee7159 100644 --- a/docs/sdk/v3/guides/swaps/03-simulate-offchain.md +++ b/docs/sdk/v3/guides/swaps/03-simulate-offchain.md @@ -23,6 +23,12 @@ The guide will **cover**: At the end of the guide, we should be able to initialize Pools with full tickdata and simulate trades offchain. +:::info +The SDKs that are used in the guide are now published by the [Uniswap Foundation](https://github.com/uniswapfoundation) instead of Uniswap Labs. +You can find a list of supported SDKs [here](https://www.npmjs.com/org/uniswapfoundation). +Make sure you don't mix SDKs published by Uniswap Labs and the Uniswap Foundation to avoid unpredictable behavior. +::: + For this guide, the following Uniswap packages are used: - [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk) diff --git a/docs/sdk/v3/guides/swaps/04-routing.md b/docs/sdk/v3/guides/swaps/04-routing.md index 3c3c775084..b130ee3ce3 100644 --- a/docs/sdk/v3/guides/swaps/04-routing.md +++ b/docs/sdk/v3/guides/swaps/04-routing.md @@ -21,6 +21,12 @@ The guide will **cover**: At the end of the guide, we should be able to create a route and and execute a swap between any two currencies tokens using the example's included UI. +:::info +The SDKs that are used in the guide are now published by the [Uniswap Foundation](https://github.com/uniswapfoundation) instead of Uniswap Labs. +You can find a list of supported SDKs [here](https://www.npmjs.com/org/uniswapfoundation). +Make sure you don't mix SDKs published by Uniswap Labs and the Uniswap Foundation to avoid unpredictable behavior. +::: + For this guide, the following Uniswap packages are used: - [`@uniswapfoundation/v3-sdk`](https://www.npmjs.com/package/@uniswapfoundation/v3-sdk)