From 32e679c62ea607f00753345ad5fa93f922d2fa42 Mon Sep 17 00:00:00 2001 From: Jordan Frankfurt Date: Tue, 23 Mar 2021 20:18:52 -0400 Subject: [PATCH] prototype position data hook (#32) Co-authored-by: Jordan Frankfurt --- package.json | 1 + src/components/PositionList/index.tsx | 10 +-- src/connectors/index.ts | 2 +- src/constants/v3/index.ts | 28 ++++++++ src/hooks/useContract.ts | 9 +++ src/hooks/useV3PositionManager.ts | 63 +++++++++++++++++ src/pages/Pool/index.tsx | 67 +++++-------------- src/state/lists/hooks.ts | 4 +- src/state/multicall/hooks.ts | 2 +- .../contracts/NonfungiblePositionManager.d.ts | 6 ++ src/types/v3/index.d.ts | 17 +++++ yarn.lock | 18 +++++ 12 files changed, 163 insertions(+), 64 deletions(-) create mode 100644 src/constants/v3/index.ts create mode 100644 src/hooks/useV3PositionManager.ts create mode 100644 src/types/v3/contracts/NonfungiblePositionManager.d.ts create mode 100644 src/types/v3/index.d.ts diff --git a/package.json b/package.json index dd18e3e33..dfde4af59 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@uniswap/token-lists": "^1.0.0-beta.19", "@uniswap/v2-core": "1.0.0", "@uniswap/v2-periphery": "^1.1.0-beta.0", + "@uniswap/v3-periphery": "^1.0.0-beta.7", "@web3-react/core": "^6.0.9", "@web3-react/fortmatic-connector": "^6.0.9", "@web3-react/injected-connector": "^6.0.7", diff --git a/src/components/PositionList/index.tsx b/src/components/PositionList/index.tsx index d3d02944f..96abbf310 100644 --- a/src/components/PositionList/index.tsx +++ b/src/components/PositionList/index.tsx @@ -8,6 +8,7 @@ import { unwrappedToken } from 'utils/wrappedCurrency' import styled, { keyframes } from 'styled-components' import { Link } from 'react-router-dom' import { MEDIA_WIDTHS } from 'theme' +import { Position } from 'types/v3' const ActiveDot = styled.span` background-color: ${({ theme }) => theme.success}; @@ -175,15 +176,6 @@ const DataText = styled.div` font-weight: 500; ` -interface Position { - feeLevel: Percent - feesEarned: Record - tokenAmount0: TokenAmount - tokenAmount1: TokenAmount - tickLower: number - tickUpper: number -} - export type PositionListProps = React.PropsWithChildren<{ loading: boolean positions: Position[] diff --git a/src/connectors/index.ts b/src/connectors/index.ts index 0817c78a8..35a89cef3 100644 --- a/src/connectors/index.ts +++ b/src/connectors/index.ts @@ -27,7 +27,7 @@ export function getNetworkLibrary(): Web3Provider { } export const injected = new InjectedConnector({ - supportedChainIds: [1, 3, 4, 5, 42], + supportedChainIds: [1, 3, 4, 5, 42, 1337], }) // mainnet only diff --git a/src/constants/v3/index.ts b/src/constants/v3/index.ts new file mode 100644 index 000000000..2d3c4f3ca --- /dev/null +++ b/src/constants/v3/index.ts @@ -0,0 +1,28 @@ +import { ChainId } from '@uniswap/sdk' + +export const NONFUNGIBLE_POSITION_MANAGER_ADDRESSES: { [chainId in ChainId | 1337]: string } = { + [ChainId.MAINNET]: '', + [ChainId.ROPSTEN]: '', + [ChainId.RINKEBY]: '', + [ChainId.GÖRLI]: '', + [ChainId.KOVAN]: '', + [1337]: '0xee9e30637f84Bbf929042A9118c6E20023dab833', +} + +export const NONFUNGIBLE_TOKEN_POSITION_DESCRIPTOR_ADDRESSES: { [chainId in ChainId | 1337]: string } = { + [ChainId.MAINNET]: '', + [ChainId.ROPSTEN]: '', + [ChainId.RINKEBY]: '', + [ChainId.GÖRLI]: '', + [ChainId.KOVAN]: '', + [1337]: '0x3431b9Ed12e3204bC6f7039e1c576417B70fdD67', +} + +export const SWAP_ROUTER_ADDRESSES: { [chainId in ChainId | 1337]: string } = { + [ChainId.MAINNET]: '', + [ChainId.ROPSTEN]: '', + [ChainId.RINKEBY]: '', + [ChainId.GÖRLI]: '', + [ChainId.KOVAN]: '', + [1337]: '0xa0588c89Fe967e66533aB1A0504C30989f90156f', +} diff --git a/src/hooks/useContract.ts b/src/hooks/useContract.ts index a4f85ae30..bcaf57893 100644 --- a/src/hooks/useContract.ts +++ b/src/hooks/useContract.ts @@ -5,7 +5,10 @@ import { abi as STAKING_REWARDS_ABI } from '@uniswap/liquidity-staker/build/Stak import { abi as MERKLE_DISTRIBUTOR_ABI } from '@uniswap/merkle-distributor/build/MerkleDistributor.json' import { ChainId, WETH } from '@uniswap/sdk' import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json' +import { abi as NFTPositionManagerABI } from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json' +import { NONFUNGIBLE_POSITION_MANAGER_ADDRESSES } from 'constants/v3' import { useMemo } from 'react' +import { NonfungiblePositionManager } from 'types/v3/contracts/NonfungiblePositionManager' import { GOVERNANCE_ADDRESS, MERKLE_DISTRIBUTOR_ADDRESS, UNI } from '../constants' import { ARGENT_WALLET_DETECTOR_ABI, @@ -128,3 +131,9 @@ export function useSocksController(): Contract | null { false ) } + +export function useV3NFTPositionManagerContract(): NonfungiblePositionManager | null { + const { chainId } = useActiveWeb3React() + const address = chainId ? NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId] : undefined + return useContract(address, NFTPositionManagerABI) as NonfungiblePositionManager | null +} diff --git a/src/hooks/useV3PositionManager.ts b/src/hooks/useV3PositionManager.ts new file mode 100644 index 000000000..7ed979b18 --- /dev/null +++ b/src/hooks/useV3PositionManager.ts @@ -0,0 +1,63 @@ +import { OptionalMethodInputs, useSingleCallResult, useSingleContractMultipleData } from 'state/multicall/hooks' +import { Position } from 'types/v3' +import { useV3NFTPositionManagerContract } from './useContract' + +interface UseV3PositionsResults { + error?: (string | boolean) | (string | boolean)[] + loading: boolean + positions: Position[] +} +export function useV3Positions(account: string | null | undefined): UseV3PositionsResults { + const positionManager = useV3NFTPositionManagerContract() + let loading = false + let error: any + const { + error: balanceOfError, + loading: balanceOfLoading, + result: balanceOfResult, + } = useSingleCallResult(positionManager, 'balanceOf', [account || undefined]) + + loading = balanceOfLoading + error = balanceOfError + + const tokenOfOwnerByIndexArgs: OptionalMethodInputs[] = balanceOfResult + ? balanceOfResult.filter((x) => Boolean(x)).map((index) => [account, index]) + : [] + const tokensCallResults = useSingleContractMultipleData( + positionManager, + 'tokenOfOwnerByIndex', + tokenOfOwnerByIndexArgs + ) + + const callData: any[] = [] + tokensCallResults.forEach(({ error: e, loading: l, result: data }) => { + if (e && !error) { + error = e + } + loading = loading || l + if (data) { + callData.push([account, data]) + } + }) + + const positionsCallResults = useSingleContractMultipleData(positionManager, 'positions', callData) + + const positions: any[] = [] + positionsCallResults.forEach(({ error: e, loading: l, result: data }) => { + if (e) { + if (!error) { + error = e + } + if (error && Array.isArray(error)) { + error = [...error, error] + } + } + loading = loading || l + + if (data) { + positions.push(data) + } + }) + + return { error, loading, positions } +} diff --git a/src/pages/Pool/index.tsx b/src/pages/Pool/index.tsx index 5eb7af882..fc0d9249e 100644 --- a/src/pages/Pool/index.tsx +++ b/src/pages/Pool/index.tsx @@ -1,4 +1,3 @@ -import { TokenAmount } from '@uniswap/sdk' import Badge, { BadgeVariant } from 'components/Badge' import { ButtonGray, ButtonPrimary } from 'components/Button' import { AutoColumn } from 'components/Column' @@ -7,6 +6,7 @@ import { SwapPoolTabs } from 'components/NavigationTabs' import PositionList from 'components/PositionList' import { RowBetween, RowFixed } from 'components/Row' import { useActiveWeb3React } from 'hooks' +import { useV3Positions } from 'hooks/useV3PositionManager' import React, { useContext, useMemo } from 'react' import { BookOpen, ChevronDown, Download, Inbox, Info, PlusCircle } from 'react-feather' import { useTranslation } from 'react-i18next' @@ -14,8 +14,6 @@ import { Link } from 'react-router-dom' import { useWalletModalToggle } from 'state/application/hooks' import styled, { ThemeContext } from 'styled-components' import { HideSmall, MEDIA_WIDTHS, TYPE } from 'theme' -import { basisPointsToPercent } from 'utils' -import { DAI, WBTC } from '../../constants' const PageWrapper = styled(AutoColumn)` max-width: 870px; @@ -91,64 +89,29 @@ const MainContentWrapper = styled.main` display: flex; flex-direction: column; ` -const FEE_BIPS = { - FIVE: basisPointsToPercent(5), - THIRTY: basisPointsToPercent(30), - ONE_HUNDRED: basisPointsToPercent(100), -} - -function useV3Positions() { - const positions = [ - { - feesEarned: { - DAI: 1000, - WBTC: 0.005, - }, - feeLevel: FEE_BIPS.FIVE, - tokenAmount0: new TokenAmount(DAI, BigInt(0) * BigInt(10e18)), - tokenAmount1: new TokenAmount(WBTC, BigInt(1) * BigInt(10e7)), - tickLower: 40000, - tickUpper: 60000, - }, - { - feesEarned: { - DAI: 1000, - WBTC: 0.005, - }, - feeLevel: FEE_BIPS.THIRTY, - tokenAmount0: new TokenAmount(DAI, BigInt(5000) * BigInt(10e18)), - tokenAmount1: new TokenAmount(WBTC, BigInt(1) * BigInt(10e7)), - tickLower: 45000, - tickUpper: 55000, - }, - ] - const error = undefined - const loading = false - return { error, loading, positions } -} export default function Pool() { - const { error, loading, positions } = useV3Positions() + const { account } = useActiveWeb3React() + const { error, loading, positions } = useV3Positions(account) const toggleWalletModal = useWalletModalToggle() const { t } = useTranslation() const theme = useContext(ThemeContext) - const { account } = useActiveWeb3React() if (error) { - console.error(error) + console.error('error fetching v3 positions', error) } - const numInactivePositions = useMemo( - () => - positions.reduce((acc: any, position: any) => { - const { tokenAmount0, tokenAmount1 } = position - const limitCrossed = tokenAmount0.equalTo(BigInt(0)) || tokenAmount1.equalTo(BigInt(0)) - return limitCrossed ? acc + 1 : acc - }, 0), - [positions] - ) + const hasPositions = Boolean(positions?.length > 0) + + const numInactivePositions = useMemo(() => { + return positions.reduce((acc: any, position: any) => { + const { tokenAmount0, tokenAmount1 } = position + const limitCrossed = tokenAmount0.equalTo(BigInt(0)) || tokenAmount1.equalTo(BigInt(0)) + return limitCrossed ? acc + 1 : acc + }, 0) + }, [positions]) const hasV2Liquidity = true - const showMigrateHeaderLink = hasV2Liquidity && positions.length > 0 + const showMigrateHeaderLink = Boolean(hasV2Liquidity && hasPositions) const menuItems = [ { @@ -219,7 +182,7 @@ export default function Pool() { - {positions?.length > 0 ? ( + {hasPositions ? ( ) : ( diff --git a/src/state/lists/hooks.ts b/src/state/lists/hooks.ts index 5ecf75abd..0dba358f5 100644 --- a/src/state/lists/hooks.ts +++ b/src/state/lists/hooks.ts @@ -30,7 +30,7 @@ export class WrappedTokenInfo extends Token { } export type TokenAddressMap = Readonly< - { [chainId in ChainId]: Readonly<{ [tokenAddress: string]: { token: WrappedTokenInfo; list: TokenList } }> } + { [chainId in ChainId | 1337]: Readonly<{ [tokenAddress: string]: { token: WrappedTokenInfo; list: TokenList } }> } > /** @@ -42,6 +42,7 @@ const EMPTY_LIST: TokenAddressMap = { [ChainId.ROPSTEN]: {}, [ChainId.GÖRLI]: {}, [ChainId.MAINNET]: {}, + [1337]: {}, } const listCache: WeakMap | null = @@ -97,6 +98,7 @@ function combineMaps(map1: TokenAddressMap, map2: TokenAddressMap): TokenAddress 4: { ...map1[4], ...map2[4] }, 5: { ...map1[5], ...map2[5] }, 42: { ...map1[42], ...map2[42] }, + 1337: { ...map1[1337], ...map2[1337] }, } } diff --git a/src/state/multicall/hooks.ts b/src/state/multicall/hooks.ts index 163f859d3..7a677caaf 100644 --- a/src/state/multicall/hooks.ts +++ b/src/state/multicall/hooks.ts @@ -22,7 +22,7 @@ export interface Result extends ReadonlyArray { type MethodArg = string | number | BigNumber type MethodArgs = Array -type OptionalMethodInputs = Array | undefined +export type OptionalMethodInputs = Array | undefined function isMethodArg(x: unknown): x is MethodArg { return ['string', 'number'].indexOf(typeof x) !== -1 diff --git a/src/types/v3/contracts/NonfungiblePositionManager.d.ts b/src/types/v3/contracts/NonfungiblePositionManager.d.ts new file mode 100644 index 000000000..a25631f90 --- /dev/null +++ b/src/types/v3/contracts/NonfungiblePositionManager.d.ts @@ -0,0 +1,6 @@ +import { Contract } from '@ethersproject/contracts' + +export interface NonfungiblePositionManager extends Contract { + balanceOf(address: string): Promise + tokenOfOwnerByIndex(address: string, index: BigNumber): Promise +} diff --git a/src/types/v3/index.d.ts b/src/types/v3/index.d.ts new file mode 100644 index 000000000..1cd2e643d --- /dev/null +++ b/src/types/v3/index.d.ts @@ -0,0 +1,17 @@ +import { BigNumberish } from '@ethersproject/bignumber' +import { basisPointsToPercent } from 'utils' + +const FEE_BIPS = { + FIVE: basisPointsToPercent(5), + THIRTY: basisPointsToPercent(30), + ONE_HUNDRED: basisPointsToPercent(100), +} + +export interface Position { + feesEarned: Record + feeLevel: FEE_BIPS + tokenAmount0: TokenAmount + tokenAmount1: TokenAmount + tickLower: BigNumberish + tickUpper: BigNumberish +} diff --git a/yarn.lock b/yarn.lock index 5863a9dd5..73e1c9499 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2257,6 +2257,11 @@ mkdirp "^1.0.4" rimraf "^3.0.2" +"@openzeppelin/contracts@3.4.1-solc-0.7-2": + version "3.4.1-solc-0.7-2" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.1-solc-0.7-2.tgz#371c67ebffe50f551c3146a9eec5fe6ffe862e92" + integrity sha512-tAG9LWg8+M2CMu7hIsqHPaTyG4uDzjr6mhvH96LvOpLZZj6tgzTluBt+LsCf1/QaYrlis6pITvpIaIhE+iZB+Q== + "@pedrouid/iso-crypto@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@pedrouid/iso-crypto/-/iso-crypto-1.0.0.tgz#cf06b40ef3da3d7ca7363bd7a521ed59fa2fd13d" @@ -3984,6 +3989,19 @@ "@uniswap/lib" "1.1.1" "@uniswap/v2-core" "1.0.0" +"@uniswap/v3-core@^1.0.0-beta.10": + version "1.0.0-beta.10" + resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.0-beta.10.tgz#c0fa1169e4a7fd0e5b33efb827c95c9bd05be071" + integrity sha512-Z23ikJr3sWIYQrvsRMrzDCeDXfQtRQMIyGsgHtfx4UAXc4jjUzNfDxNdAcBcgFH0u0ekt1RJ0gXUVcH0NjPfKQ== + +"@uniswap/v3-periphery@^1.0.0-beta.7": + version "1.0.0-beta.7" + resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.0.0-beta.7.tgz#d110308f8f8d0d645ad541ebe0f62721ade0dbd8" + integrity sha512-tvOYdjVgrYzIuxIjUHCyYv8C0UYwa0PFkn0xM0FALU2TNYy7BP4ITrozu6KmUBqEVZf9hAs2HlmomhMoSa/WtA== + dependencies: + "@openzeppelin/contracts" "3.4.1-solc-0.7-2" + "@uniswap/v3-core" "^1.0.0-beta.10" + "@walletconnect/client@^1.1.1-alpha.0": version "1.3.6" resolved "https://registry.yarnpkg.com/@walletconnect/client/-/client-1.3.6.tgz#537b7af6bf87a906fcf171fd5bc4e56a2a3d1908"