Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
587b659816 | ||
|
|
5388cab779 | ||
|
|
cadd68fb37 | ||
|
|
ab8ce37ceb | ||
|
|
5a9a71ae2d | ||
|
|
b93fd230bd | ||
|
|
e9a11bb604 | ||
|
|
c5afbedb3e | ||
|
|
5b2c44522e | ||
|
|
7fd4005154 | ||
|
|
48eab0d0ac | ||
|
|
c9ee1b3b32 | ||
|
|
eb4c305eff | ||
|
|
d9825622f1 | ||
|
|
8975086a69 | ||
|
|
ddf88345a9 | ||
|
|
32ac25556b | ||
|
|
50a599c005 |
@@ -31,7 +31,7 @@
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
"@uniswap/merkle-distributor": "1.0.1",
|
||||
"@uniswap/sdk": "3.0.3",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.17",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.19",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@web3-react/core": "^6.0.9",
|
||||
|
||||
@@ -39,5 +39,6 @@
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "a141524a42594f028627f6004b23ff75"}'></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
12
src/components/Blocklist/index.tsx
Normal file
12
src/components/Blocklist/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React, { ReactNode, useMemo } from 'react'
|
||||
import { BLOCKED_ADDRESSES } from '../../constants'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
|
||||
export default function Blocklist({ children }: { children: ReactNode }) {
|
||||
const { account } = useActiveWeb3React()
|
||||
const blocked: boolean = useMemo(() => Boolean(account && BLOCKED_ADDRESSES.indexOf(account) !== -1), [account])
|
||||
if (blocked) {
|
||||
return <div>Blocked address</div>
|
||||
}
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -159,6 +159,26 @@ export const ButtonPink = styled(Base)`
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonUNIGradient = styled(ButtonPrimary)`
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
height: 36px;
|
||||
font-weight: 500;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
background: radial-gradient(174.47% 188.91% at 1.84% 0%, #ff007a 0%, #2172e5 100%), #edeef2;
|
||||
width: fit-content;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
white-space: no-wrap;
|
||||
:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonOutlined = styled(Base)`
|
||||
border: 1px solid ${({ theme }) => theme.bg2};
|
||||
background-color: transparent;
|
||||
|
||||
@@ -123,9 +123,6 @@ const AccountElement = styled.div<{ active: boolean }>`
|
||||
:focus {
|
||||
border: 1px solid blue;
|
||||
}
|
||||
/* :hover {
|
||||
background-color: ${({ theme, active }) => (!active ? theme.bg2 : theme.bg4)};
|
||||
} */
|
||||
`
|
||||
|
||||
const UNIAmount = styled(AccountElement)`
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useCallback, useMemo } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components'
|
||||
import { AppDispatch } from '../../state'
|
||||
import { useRemovePopup } from '../../state/application/hooks'
|
||||
import { acceptListUpdate } from '../../state/lists/actions'
|
||||
@@ -12,6 +13,11 @@ import { ButtonSecondary } from '../Button'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AutoRow } from '../Row'
|
||||
|
||||
export const ChangesList = styled.ul`
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
export default function ListUpdatePopup({
|
||||
popKey,
|
||||
listUrl,
|
||||
@@ -64,7 +70,7 @@ export default function ListUpdatePopup({
|
||||
An update is available for the token list "{oldList.name}" (
|
||||
{listVersionLabel(oldList.version)} to {listVersionLabel(newList.version)}).
|
||||
</Text>
|
||||
<ul>
|
||||
<ChangesList>
|
||||
{tokensAdded.length > 0 ? (
|
||||
<li>
|
||||
{tokensAdded.map((token, i) => (
|
||||
@@ -88,7 +94,7 @@ export default function ListUpdatePopup({
|
||||
</li>
|
||||
) : null}
|
||||
{numTokensChanged > 0 ? <li>{numTokensChanged} tokens updated</li> : null}
|
||||
</ul>
|
||||
</ChangesList>
|
||||
</div>
|
||||
<AutoRow>
|
||||
<div style={{ flexGrow: 1, marginRight: 12 }}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { JSBI, Pair, Percent } from '@uniswap/sdk'
|
||||
import { JSBI, Pair, Percent, TokenAmount } from '@uniswap/sdk'
|
||||
import { darken } from 'polished'
|
||||
import React, { useState } from 'react'
|
||||
import { ChevronDown, ChevronUp } from 'react-feather'
|
||||
@@ -9,10 +9,10 @@ import { useTotalSupply } from '../../data/TotalSupply'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useTokenBalance } from '../../state/wallet/hooks'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { ExternalLink, TYPE, HideExtraSmall, ExtraSmallOnly } from '../../theme'
|
||||
import { currencyId } from '../../utils/currencyId'
|
||||
import { unwrappedToken } from '../../utils/wrappedCurrency'
|
||||
import { ButtonPrimary, ButtonSecondary, ButtonEmpty } from '../Button'
|
||||
import { ButtonPrimary, ButtonSecondary, ButtonEmpty, ButtonUNIGradient } from '../Button'
|
||||
import { transparentize } from 'polished'
|
||||
import { CardNoise } from '../earn/styled'
|
||||
|
||||
@@ -22,8 +22,9 @@ import Card, { GreyCard, LightCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import DoubleCurrencyLogo from '../DoubleLogo'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import { RowBetween, RowFixed, AutoRow } from '../Row'
|
||||
import { Dots } from '../swap/styleds'
|
||||
import { BIG_INT_ZERO } from '../../constants'
|
||||
|
||||
export const FixedHeightRow = styled(RowBetween)`
|
||||
height: 24px;
|
||||
@@ -47,6 +48,7 @@ interface PositionCardProps {
|
||||
pair: Pair
|
||||
showUnwrapped?: boolean
|
||||
border?: string
|
||||
stakedBalance?: TokenAmount // optional balance to indicate that liquidity is deposited in mining pool
|
||||
}
|
||||
|
||||
export function MinimalPositionCard({ pair, showUnwrapped = false, border }: PositionCardProps) {
|
||||
@@ -157,7 +159,7 @@ export function MinimalPositionCard({ pair, showUnwrapped = false, border }: Pos
|
||||
)
|
||||
}
|
||||
|
||||
export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||
export default function FullPositionCard({ pair, border, stakedBalance }: PositionCardProps) {
|
||||
const { account } = useActiveWeb3React()
|
||||
|
||||
const currency0 = unwrappedToken(pair.token0)
|
||||
@@ -165,9 +167,12 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||
|
||||
const [showMore, setShowMore] = useState(false)
|
||||
|
||||
const userPoolBalance = useTokenBalance(account ?? undefined, pair.liquidityToken)
|
||||
const userDefaultPoolBalance = useTokenBalance(account ?? undefined, pair.liquidityToken)
|
||||
const totalPoolTokens = useTotalSupply(pair.liquidityToken)
|
||||
|
||||
// if staked balance balance provided, add to standard liquidity amount
|
||||
const userPoolBalance = stakedBalance ? userDefaultPoolBalance?.add(stakedBalance) : userDefaultPoolBalance
|
||||
|
||||
const poolTokenPercentage =
|
||||
!!userPoolBalance && !!totalPoolTokens && JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
|
||||
? new Percent(userPoolBalance.raw, totalPoolTokens.raw)
|
||||
@@ -192,12 +197,22 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||
<CardNoise />
|
||||
<AutoColumn gap="12px">
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<DoubleCurrencyLogo currency0={currency0} currency1={currency1} margin={true} size={20} />
|
||||
<AutoRow gap="8px">
|
||||
<DoubleCurrencyLogo currency0={currency0} currency1={currency1} size={20} />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{!currency0 || !currency1 ? <Dots>Loading</Dots> : `${currency0.symbol}/${currency1.symbol}`}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
{!!stakedBalance && (
|
||||
<ButtonUNIGradient as={Link} to={`/uni/${currencyId(currency0)}/${currencyId(currency1)}`}>
|
||||
<HideExtraSmall>Earning UNI</HideExtraSmall>
|
||||
<ExtraSmallOnly>
|
||||
<span role="img" aria-label="bolt">
|
||||
⚡
|
||||
</span>
|
||||
</ExtraSmallOnly>
|
||||
</ButtonUNIGradient>
|
||||
)}
|
||||
</AutoRow>
|
||||
|
||||
<RowFixed gap="8px">
|
||||
<ButtonEmpty
|
||||
@@ -208,7 +223,6 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||
>
|
||||
{showMore ? (
|
||||
<>
|
||||
{' '}
|
||||
Manage
|
||||
<ChevronUp size="20" style={{ marginLeft: '10px' }} />
|
||||
</>
|
||||
@@ -226,12 +240,22 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||
<AutoColumn gap="8px">
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool tokens:
|
||||
Your total pool tokens:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
{stakedBalance && (
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Pool tokens in rewards pool:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{stakedBalance.toSignificant(4)}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
)}
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
@@ -273,7 +297,9 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||
Your pool share:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
|
||||
{poolTokenPercentage
|
||||
? (poolTokenPercentage.toFixed(2) === '0.00' ? '<0.01' : poolTokenPercentage.toFixed(2)) + '%'
|
||||
: '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
|
||||
@@ -285,26 +311,39 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||
View accrued fees and analytics<span style={{ fontSize: '11px' }}>↗</span>
|
||||
</ExternalLink>
|
||||
</ButtonSecondary>
|
||||
<RowBetween marginTop="10px">
|
||||
{userDefaultPoolBalance && JSBI.greaterThan(userDefaultPoolBalance.raw, BIG_INT_ZERO) && (
|
||||
<RowBetween marginTop="10px">
|
||||
<ButtonPrimary
|
||||
padding="8px"
|
||||
borderRadius="8px"
|
||||
as={Link}
|
||||
to={`/add/${currencyId(currency0)}/${currencyId(currency1)}`}
|
||||
width="48%"
|
||||
>
|
||||
Add
|
||||
</ButtonPrimary>
|
||||
<ButtonPrimary
|
||||
padding="8px"
|
||||
borderRadius="8px"
|
||||
as={Link}
|
||||
width="48%"
|
||||
to={`/remove/${currencyId(currency0)}/${currencyId(currency1)}`}
|
||||
>
|
||||
Remove
|
||||
</ButtonPrimary>
|
||||
</RowBetween>
|
||||
)}
|
||||
{stakedBalance && JSBI.greaterThan(stakedBalance.raw, BIG_INT_ZERO) && (
|
||||
<ButtonPrimary
|
||||
padding="8px"
|
||||
borderRadius="8px"
|
||||
as={Link}
|
||||
to={`/add/${currencyId(currency0)}/${currencyId(currency1)}`}
|
||||
width="48%"
|
||||
to={`/uni/${currencyId(currency0)}/${currencyId(currency1)}`}
|
||||
width="100%"
|
||||
>
|
||||
Add
|
||||
Manage Liquidity in Rewards Pool
|
||||
</ButtonPrimary>
|
||||
<ButtonPrimary
|
||||
padding="8px"
|
||||
borderRadius="8px"
|
||||
as={Link}
|
||||
width="48%"
|
||||
to={`/remove/${currencyId(currency0)}/${currencyId(currency1)}`}
|
||||
>
|
||||
Remove
|
||||
</ButtonPrimary>
|
||||
</RowBetween>
|
||||
)}
|
||||
</AutoColumn>
|
||||
)}
|
||||
</AutoColumn>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { unwrappedToken } from '../../utils/wrappedCurrency'
|
||||
import { useTotalSupply } from '../../data/TotalSupply'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import useUSDCPrice from '../../utils/useUSDCPrice'
|
||||
import { BIG_INT_SECONDS_IN_WEEK } from '../../constants'
|
||||
|
||||
const StatContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -56,11 +57,6 @@ const TopSection = styled.div`
|
||||
`};
|
||||
`
|
||||
|
||||
// const APR = styled.div`
|
||||
// display: flex;
|
||||
// justify-content: flex-end;
|
||||
// `
|
||||
|
||||
const BottomSection = styled.div<{ showBackground: boolean }>`
|
||||
padding: 12px 16px;
|
||||
opacity: ${({ showBackground }) => (showBackground ? '1' : '0.4')};
|
||||
@@ -139,9 +135,15 @@ export default function PoolCard({ stakingInfo }: { stakingInfo: StakingInfo })
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<TYPE.white> Pool rate </TYPE.white>
|
||||
<TYPE.white>{`${stakingInfo.totalRewardRate
|
||||
?.multiply(`${60 * 60 * 24 * 7}`)
|
||||
?.toFixed(0, { groupSeparator: ',' })} UNI / week`}</TYPE.white>
|
||||
<TYPE.white>
|
||||
{stakingInfo
|
||||
? stakingInfo.active
|
||||
? `${stakingInfo.totalRewardRate
|
||||
?.multiply(BIG_INT_SECONDS_IN_WEEK)
|
||||
?.toFixed(0, { groupSeparator: ',' })} UNI / week`
|
||||
: '0 UNI / week'
|
||||
: '-'}
|
||||
</TYPE.white>
|
||||
</RowBetween>
|
||||
</StatContainer>
|
||||
|
||||
@@ -157,9 +159,13 @@ export default function PoolCard({ stakingInfo }: { stakingInfo: StakingInfo })
|
||||
<span role="img" aria-label="wizard-icon" style={{ marginRight: '0.5rem' }}>
|
||||
⚡
|
||||
</span>
|
||||
{`${stakingInfo.rewardRate
|
||||
?.multiply(`${60 * 60 * 24 * 7}`)
|
||||
?.toSignificant(4, { groupSeparator: ',' })} UNI / week`}
|
||||
{stakingInfo
|
||||
? stakingInfo.active
|
||||
? `${stakingInfo.rewardRate
|
||||
?.multiply(BIG_INT_SECONDS_IN_WEEK)
|
||||
?.toSignificant(4, { groupSeparator: ',' })} UNI / week`
|
||||
: '0 UNI / week'
|
||||
: '-'}
|
||||
</TYPE.black>
|
||||
</BottomSection>
|
||||
</>
|
||||
|
||||
@@ -7,6 +7,8 @@ export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
|
||||
|
||||
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
|
||||
|
||||
export { PRELOADED_PROPOSALS } from './proposals'
|
||||
|
||||
// a list of tokens by chain
|
||||
type ChainTokenList = {
|
||||
readonly [chainId in ChainId]: Token[]
|
||||
@@ -18,13 +20,17 @@ export const USDT = new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597
|
||||
export const COMP = new Token(ChainId.MAINNET, '0xc00e94Cb662C3520282E6f5717214004A7f26888', 18, 'COMP', 'Compound')
|
||||
export const MKR = new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker')
|
||||
export const AMPL = new Token(ChainId.MAINNET, '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 9, 'AMPL', 'Ampleforth')
|
||||
export const WBTC = new Token(ChainId.MAINNET, '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 18, 'WBTC', 'Wrapped BTC')
|
||||
export const WBTC = new Token(ChainId.MAINNET, '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 8, 'WBTC', 'Wrapped BTC')
|
||||
|
||||
// TODO this is only approximate, it's actually based on blocks
|
||||
export const PROPOSAL_LENGTH_IN_DAYS = 7
|
||||
// Block time here is slightly higher (~1s) than average in order to avoid ongoing proposals past the displayed time
|
||||
export const AVERAGE_BLOCK_TIME_IN_SECS = 13
|
||||
export const PROPOSAL_LENGTH_IN_BLOCKS = 40_320
|
||||
export const PROPOSAL_LENGTH_IN_SECS = AVERAGE_BLOCK_TIME_IN_SECS * PROPOSAL_LENGTH_IN_BLOCKS
|
||||
|
||||
export const GOVERNANCE_ADDRESS = '0x5e4be8Bc9637f0EAA1A755019e06A68ce081D58F'
|
||||
|
||||
export const TIMELOCK_ADDRESS = '0x1a9C8182C09F50C8318d769245beA52c32BE35BC'
|
||||
|
||||
const UNI_ADDRESS = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'
|
||||
export const UNI: { [chainId in ChainId]: Token } = {
|
||||
[ChainId.MAINNET]: new Token(ChainId.MAINNET, UNI_ADDRESS, 18, 'UNI', 'Uniswap'),
|
||||
@@ -35,9 +41,9 @@ export const UNI: { [chainId in ChainId]: Token } = {
|
||||
}
|
||||
|
||||
export const COMMON_CONTRACT_NAMES: { [address: string]: string } = {
|
||||
'0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984': 'UNI',
|
||||
'0x5e4be8Bc9637f0EAA1A755019e06A68ce081D58F': 'Governance Alpha',
|
||||
'0x1a9C8182C09F50C8318d769245beA52c32BE35BC': 'Timelock'
|
||||
[UNI_ADDRESS]: 'UNI',
|
||||
[GOVERNANCE_ADDRESS]: 'Governance',
|
||||
[TIMELOCK_ADDRESS]: 'Timelock'
|
||||
}
|
||||
|
||||
// TODO: specify merkle distributor for mainnet
|
||||
@@ -175,6 +181,9 @@ export const INITIAL_ALLOWED_SLIPPAGE = 50
|
||||
// 20 minutes, denominated in seconds
|
||||
export const DEFAULT_DEADLINE_FROM_NOW = 60 * 20
|
||||
|
||||
// used for rewards deadlines
|
||||
export const BIG_INT_SECONDS_IN_WEEK = JSBI.BigInt(60 * 60 * 24 * 7)
|
||||
|
||||
export const BIG_INT_ZERO = JSBI.BigInt(0)
|
||||
|
||||
// one basis point
|
||||
@@ -192,3 +201,11 @@ export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt(
|
||||
// used to ensure the user doesn't send so much ETH so they end up with <.01
|
||||
export const MIN_ETH: JSBI = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(16)) // .01 ETH
|
||||
export const BETTER_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
|
||||
|
||||
// SDN OFAC addresses
|
||||
export const BLOCKED_ADDRESSES: string[] = [
|
||||
'0x7F367cC41522cE07553e823bf3be79A889DEbe1B',
|
||||
'0xd882cFc20F52f2599D84b8e8D58C7FB62cfE344b',
|
||||
'0x901bb9583b24D97e995513C6778dc6888AB6870e',
|
||||
'0xA7e5d5A720f06526557c513402f2e6B5fA20b008'
|
||||
]
|
||||
|
||||
4
src/constants/proposals/index.ts
Normal file
4
src/constants/proposals/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { UNISWAP_GRANTS } from './uniswap_grants'
|
||||
|
||||
// Proposals are 0-indexed
|
||||
export const PRELOADED_PROPOSALS = new Map([[2, UNISWAP_GRANTS]])
|
||||
106
src/constants/proposals/uniswap_grants.ts
Normal file
106
src/constants/proposals/uniswap_grants.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
export const UNISWAP_GRANTS = `# Uniswap Grants Program v0.1
|
||||
|
||||
*co-authored with [Ken Ng](https://twitter.com/nkennethk?lang=en)*
|
||||
|
||||
## Summary:
|
||||
|
||||
**This post outlines a framework for funding Uniswap ecosystem development with grants from the[ UNI Community Treasury](https://uniswap.org/blog/uni/).** The program starts small—sponsoring hackathons, [for example](https://gov.uniswap.org/c/proposal-discussion/5)—but could grow in significance over time (with renewals approved by governance) to fund core protocol development. Grants administration is a subjective process that cannot be easily automated, and thus we propose a nimble committee of 6 members —1 lead and 5 reviewers—to deliver an efficient, predictable process to applicants, such that funding can be administered without having to put each application to a vote. We propose the program start with an initial cap of $750K per quarter and a limit of 2 quarters before renewal—a sum that we feel is appropriate for an MVP relative to the size of the treasury that UNI token holders are entrusted with allocating.
|
||||
|
||||
**Purpose:**
|
||||
|
||||
**The mission of the UGP is to provide valuable resources to help grow the Uniswap ecosystem.** Through public discourse and inbound applications, the community will get first-hand exposure to identify and respond to the most pressing needs of the ecosystem, as well as the ability to support innovative projects expanding the capabilities of Uniswap. By rewarding talent early with developer incentives, bounties, and infrastructure support, UGP acts as a catalyst for growth and helps to maintain Uniswap as a nexus for DeFi on Ethereum.
|
||||
|
||||
**Quarterly Budget:**
|
||||
|
||||
* Max quarterly budget of up to $750k
|
||||
* Budget and caps to be assessed every six months
|
||||
|
||||
**Grant Allocation Committee:**
|
||||
|
||||
* Of 6 committee members: 1 lead and 5 reviewers
|
||||
* Each committee has a term of 2 quarters (6 months) after which the program needs to be renewed by UNI governance
|
||||
* Committee functions as a 4 of 5 multi-sig
|
||||
|
||||
**Committee Members**
|
||||
|
||||
While the goals and priorities of the grant program will be thoroughly discussed and reviewed by the community through public discourse, **the decision to start UGP by operating as a small committee is to ensure that the application and decision process will be efficient and predictable, so applicants have clear objectives and timely decisions.**
|
||||
|
||||
Starting with just six members enables the committee to efficiently fund projects with tight feedback loops and rapid iterations. The purpose of this committee would be to test the hypothesis that the Grants Program can successfully provide value for the UNI ecosystem before scaling the program.
|
||||
|
||||
**We suggest the grants program is administered by a single lead. Here we propose Kenneth Ng, a co-author of this post**. Ken has helped grow the Ethereum Foundation Grants Program over the last two years in establishing high level priorities and adapting for the ecosystems needs.
|
||||
|
||||
**The other 5 committee members should be thought of as “reviewers” — UNI community members who will keep the grants program functional by ensuring Ken is leading effectively and, of course, not absconding with funds.** Part of the reviewers job is to hold the program accountable for success (defined below) and/or return any excess funds to the UNI treasury. Reviewers are not compensated as part of this proposal as we expect their time commitment to be minimal. Compensation for the lead role is discussed below, as we expect this to be a labor intensive role.
|
||||
|
||||
**Program Lead:** [Ken Ng](https://twitter.com/nkennethk?lang=en) (HL Creative Corp)
|
||||
*Ecosystem Support Program at the Ethereum Foundation*
|
||||
|
||||
1. Reviewer: [Jesse Walden](https://twitter.com/jessewldn) (o/b/o Unofficial LLC dba [Variant Fund](http://twitter.com/variantfund))
|
||||
*Founder and Investor at Variant Fund (holds UNI)*
|
||||
|
||||
2. Reviewer: [Monet Supply](https://twitter.com/MonetSupply)
|
||||
*Risk Analyst at MakerDAO*
|
||||
|
||||
3. Reviewer: [Robert Leshner](https://twitter.com/rleshner)
|
||||
*Founder and CEO of Compound Finance*
|
||||
|
||||
4. Reviewer: [Kain Warwick](https://twitter.com/kaiynne)
|
||||
*Founder of Synthetix*
|
||||
|
||||
5. Reviewer: [Ashleigh Schap](https://twitter.com/ashleighschap)
|
||||
*Growth Lead, Uniswap (Company)*
|
||||
|
||||
## Methodology
|
||||
|
||||
**1.1 Budget**
|
||||
|
||||
This proposal recommends a max cap of $750K per quarter, with the ability to reevaluate biannually at each epoch (two fiscal quarters). While the UGP Grants Committee will be the decision makers around individual grants, respective budgets, roadmaps, and milestones, any top-level changes to UGP including epochs and max cap, will require full community quorum (4% approval).
|
||||
|
||||
The UGP will be funded by the UNI treasury according to the[ release schedule](https://uniswap.org/blog/uni/) set out by the Uniswap team, whereby 43% of the UNI treasury is released over a four-year timeline. In Year 1 this will total to 172,000,000 UNI (~$344M as of writing).
|
||||
|
||||
Taking into consideration the current landscape of ecosystem funding across Ethereum, the community would be hard-pressed to allocate even 5% of Year 1’s treasury. For context Gitcoin CLR Round 7 distributed $725k ($450k in matched) across 857 projects and YTD, Moloch has granted just under $200k but in contrast, the EF has committed to somewhere in the 8 figure range.
|
||||
|
||||
**1.2 Committee Compensation**
|
||||
|
||||
Operating a successful grants program takes considerable time and effort. Take, for instance, the EF Ecosystem Support Program, which has awarded grants since 2018, consists of an internal team at the Foundation as well as an ever increasing roster of community advisors in order to keep expanding and adapting to best serve the needs of the Ethereum ecosystem. While the structure of the allocation committee has six members, we propose that only the lead will be remunerated for their work in establishing the initial processes, vetting applications, and managing the program overall as this role is expected to be time consuming if the program is to be succesful. We propose that the other committee members be unpaid UNI ecosystem stakeholders who have an interest in seeing the protocol ecosystem continue to operate and grow.
|
||||
|
||||
**We propose the lead be compensated 25 UNI/hr (approximately $100 USD at time of this writing) capped at 30 hours/week. This compensation, along with the quarterly budget, will be allocated to the UGP multisig from the UNI treasury**. In keeping with the committee’s commitment to the community, hours and duties will be logged publicly and transparently .
|
||||
|
||||
**2.1 Priorities**
|
||||
|
||||
Initially, the program aims to start narrow in scope, funding peripheral ecosystem initiatives, such as targeted bounties, hackathon sponsorships, and other low-stakes means of building out the Uniswap developer ecosystem. Over time, if the program proves effective, the grant allocations can grow in scope to include, for example, improved frontends, trading interfaces, and eventually, core protocol development.
|
||||
|
||||

|
||||
|
||||
With the initial priorities in mind, some effective measures for quick successes might look like:
|
||||
|
||||
* Total number of projects funded
|
||||
* Quarterly increase in applications
|
||||
* Project engagement post-event/funding
|
||||
* Overall community engagement/sentiment
|
||||
|
||||
**2.2 Timeline**
|
||||
|
||||
In keeping with the fast pace of the UNI ecosystem, we organize time in epochs, or two calendar quarters. Each epoch will see two funding rounds, one per quarter, after which the community can review and create proposals to improve or revamp the program as they deem fit.
|
||||
|
||||
****
|
||||
|
||||
**Rolling Wave 1 & 2 Applications**
|
||||
|
||||
* Starting in Q1 2021, UGP will start accepting applications for events looking for support in the form of bounties or prizes that in parallel can be proactively sourced. During these first two waves of funding projects, the allocation committee lead can begin laying out guardrails for continued funding
|
||||
|
||||
* Considering the immediate feedback loops for the first epoch, we expect these allocation decisions to be discussed and reviewed by the larger community. Should this not be of value, the community can submit a formal governance proposal to change any piece of UGP that was not effective
|
||||
|
||||
**Wave 3 & Beyond**
|
||||
|
||||
* Beginning with Wave 3, there should have been enough time with impactful projects funded to be considered for follow-on funding, should it make sense. In the same vein, projects within scope will be expanded to also include those working on integrations and and other key components.
|
||||
|
||||
* Beyond the second epoch, as the community helps to review and help adapt UGP to be most effective, the scope will continue to grow in order to accommodate the state of the ecosystem including that of core protocol improvements
|
||||
|
||||
## Conclusion:
|
||||
|
||||
**If this proposal is successfully approved, UGP will start accepting applications on a rolling basis beginning at the start of 2021.** With the phases and priorities laid out above, UGP will aim to make a significant impact by catalyzing growth and innovation in the UNI ecosystem.
|
||||
|
||||
**This program is meant to be the simplest possible MVP of a Uniswap Ecosystem Grants initiative.** While the multi-sig committee comes with trust assumptions about the members, our hope is the community will approve this limited engagement to get the ball rolling in an efficient structure. **After the first epoch (2 fiscal quarters) the burden of proof will be on UGP to show empirical evidence that the program is worth continuing in its existing form and will submit to governance to renew treasury funding.**
|
||||
|
||||
If this program proves successful, we hope it will inspire others to follow suit and create their own funding committees for allocating treasury capital—ideally with different specializations.
|
||||
`
|
||||
@@ -8,6 +8,7 @@ import { isAddress } from '../utils'
|
||||
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { useBytes32TokenContract, useTokenContract } from './useContract'
|
||||
import { arrayify } from 'ethers/lib/utils'
|
||||
|
||||
export function useAllTokens(): { [address: string]: Token } {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
@@ -40,10 +41,12 @@ export function useIsUserAddedToken(currency: Currency): boolean {
|
||||
|
||||
// parse a name or symbol from a token response
|
||||
const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/
|
||||
|
||||
function parseStringOrBytes32(str: string | undefined, bytes32: string | undefined, defaultValue: string): string {
|
||||
return str && str.length > 0
|
||||
? str
|
||||
: bytes32 && BYTES32_REGEX.test(bytes32)
|
||||
: // need to check for proper bytes string and valid terminator
|
||||
bytes32 && BYTES32_REGEX.test(bytes32) && arrayify(bytes32)[31] === 0
|
||||
? parseBytes32String(bytes32)
|
||||
: defaultValue
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import ReactDOM from 'react-dom'
|
||||
import ReactGA from 'react-ga'
|
||||
import { Provider } from 'react-redux'
|
||||
import { HashRouter } from 'react-router-dom'
|
||||
import Blocklist from './components/Blocklist'
|
||||
import { NetworkContextName } from './constants'
|
||||
import './i18n'
|
||||
import App from './pages/App'
|
||||
@@ -58,15 +59,17 @@ ReactDOM.render(
|
||||
<FixedGlobalStyle />
|
||||
<Web3ReactProvider getLibrary={getLibrary}>
|
||||
<Web3ProviderNetwork getLibrary={getLibrary}>
|
||||
<Provider store={store}>
|
||||
<Updaters />
|
||||
<ThemeProvider>
|
||||
<ThemedGlobalStyle />
|
||||
<HashRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
<Blocklist>
|
||||
<Provider store={store}>
|
||||
<Updaters />
|
||||
<ThemeProvider>
|
||||
<ThemedGlobalStyle />
|
||||
<HashRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</Blocklist>
|
||||
</Web3ProviderNetwork>
|
||||
</Web3ReactProvider>
|
||||
</StrictMode>,
|
||||
|
||||
@@ -28,7 +28,7 @@ import { useTotalSupply } from '../../data/TotalSupply'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import usePrevious from '../../hooks/usePrevious'
|
||||
import useUSDCPrice from '../../utils/useUSDCPrice'
|
||||
import { BIG_INT_ZERO } from '../../constants'
|
||||
import { BIG_INT_ZERO, BIG_INT_SECONDS_IN_WEEK } from '../../constants'
|
||||
|
||||
const PageWrapper = styled(AutoColumn)`
|
||||
max-width: 640px;
|
||||
@@ -177,9 +177,11 @@ export default function Manage({
|
||||
<AutoColumn gap="sm">
|
||||
<TYPE.body style={{ margin: 0 }}>Pool Rate</TYPE.body>
|
||||
<TYPE.body fontSize={24} fontWeight={500}>
|
||||
{stakingInfo?.totalRewardRate
|
||||
?.multiply((60 * 60 * 24 * 7).toString())
|
||||
?.toFixed(0, { groupSeparator: ',' }) ?? '-'}
|
||||
{stakingInfo?.active
|
||||
? stakingInfo?.totalRewardRate
|
||||
?.multiply(BIG_INT_SECONDS_IN_WEEK)
|
||||
?.toFixed(0, { groupSeparator: ',' }) ?? '-'
|
||||
: '0'}
|
||||
{' UNI / week'}
|
||||
</TYPE.body>
|
||||
</AutoColumn>
|
||||
@@ -293,9 +295,11 @@ export default function Manage({
|
||||
<span role="img" aria-label="wizard-icon" style={{ marginRight: '8px ' }}>
|
||||
⚡
|
||||
</span>
|
||||
{stakingInfo?.rewardRate
|
||||
?.multiply((60 * 60 * 24 * 7).toString())
|
||||
?.toSignificant(4, { groupSeparator: ',' }) ?? '-'}
|
||||
{stakingInfo?.active
|
||||
? stakingInfo?.rewardRate
|
||||
?.multiply(BIG_INT_SECONDS_IN_WEEK)
|
||||
?.toSignificant(4, { groupSeparator: ',' }) ?? '-'
|
||||
: '0'}
|
||||
{' UNI / week'}
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
@@ -311,9 +315,11 @@ export default function Manage({
|
||||
|
||||
{!showAddLiquidityButton && (
|
||||
<DataRow style={{ marginBottom: '1rem' }}>
|
||||
<ButtonPrimary padding="8px" borderRadius="8px" width="160px" onClick={handleDepositClick}>
|
||||
{stakingInfo?.stakedAmount?.greaterThan(JSBI.BigInt(0)) ? 'Deposit' : 'Deposit UNI-V2 LP Tokens'}
|
||||
</ButtonPrimary>
|
||||
{stakingInfo && stakingInfo.active && (
|
||||
<ButtonPrimary padding="8px" borderRadius="8px" width="160px" onClick={handleDepositClick}>
|
||||
{stakingInfo?.stakedAmount?.greaterThan(JSBI.BigInt(0)) ? 'Deposit' : 'Deposit UNI-V2 LP Tokens'}
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
|
||||
{stakingInfo?.stakedAmount?.greaterThan(JSBI.BigInt(0)) && (
|
||||
<>
|
||||
@@ -329,7 +335,7 @@ export default function Manage({
|
||||
)}
|
||||
</DataRow>
|
||||
)}
|
||||
{!userLiquidityUnstaked ? null : userLiquidityUnstaked.equalTo('0') ? null : (
|
||||
{!userLiquidityUnstaked ? null : userLiquidityUnstaked.equalTo('0') ? null : !stakingInfo?.active ? null : (
|
||||
<TYPE.main>{userLiquidityUnstaked.toSignificant(6)} UNI-V2 LP tokens available</TYPE.main>
|
||||
)}
|
||||
</PositionInfo>
|
||||
|
||||
@@ -9,6 +9,9 @@ import { CardSection, DataCard, CardNoise, CardBGImage } from '../../components/
|
||||
import { Countdown } from './Countdown'
|
||||
import Loader from '../../components/Loader'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { JSBI } from '@uniswap/sdk'
|
||||
import { BIG_INT_ZERO } from '../../constants'
|
||||
import { OutlineCard } from '../../components/Card'
|
||||
|
||||
const PageWrapper = styled(AutoColumn)`
|
||||
max-width: 640px;
|
||||
@@ -29,16 +32,25 @@ const PoolSection = styled.div`
|
||||
justify-self: center;
|
||||
`
|
||||
|
||||
const DataRow = styled(RowBetween)`
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
flex-direction: column;
|
||||
`};
|
||||
`
|
||||
|
||||
export default function Earn() {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
// staking info for connected account
|
||||
const stakingInfos = useStakingInfo()
|
||||
|
||||
const DataRow = styled(RowBetween)`
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
flex-direction: column;
|
||||
`};
|
||||
`
|
||||
/**
|
||||
* only show staking cards with balance
|
||||
* @todo only account for this if rewards are inactive
|
||||
*/
|
||||
const stakingInfosWithBalance = stakingInfos?.filter(s => JSBI.greaterThan(s.stakedAmount.raw, BIG_INT_ZERO))
|
||||
|
||||
// toggle copy if rewards are inactive
|
||||
const stakingRewardsExist = Boolean(typeof chainId === 'number' && (STAKING_REWARDS_INFO[chainId]?.length ?? 0) > 0)
|
||||
|
||||
return (
|
||||
@@ -81,9 +93,11 @@ export default function Earn() {
|
||||
{stakingRewardsExist && stakingInfos?.length === 0 ? (
|
||||
<Loader style={{ margin: 'auto' }} />
|
||||
) : !stakingRewardsExist ? (
|
||||
'No active rewards'
|
||||
<OutlineCard>No active pools</OutlineCard>
|
||||
) : stakingInfos?.length !== 0 && stakingInfosWithBalance.length === 0 ? (
|
||||
<OutlineCard>No active pools</OutlineCard>
|
||||
) : (
|
||||
stakingInfos?.map(stakingInfo => {
|
||||
stakingInfosWithBalance?.map(stakingInfo => {
|
||||
// need to sort by added liquidity here
|
||||
return <PoolCard key={stakingInfo.stakingRewardAddress} stakingInfo={stakingInfo} />
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext, useMemo } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { Pair } from '@uniswap/sdk'
|
||||
import { Pair, JSBI } from '@uniswap/sdk'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { SwapPoolTabs } from '../../components/NavigationTabs'
|
||||
|
||||
@@ -19,6 +19,8 @@ import { usePairs } from '../../data/Reserves'
|
||||
import { toV2LiquidityToken, useTrackedTokenPairs } from '../../state/user/hooks'
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
import { CardSection, DataCard, CardNoise, CardBGImage } from '../../components/earn/styled'
|
||||
import { useStakingInfo } from '../../state/stake/hooks'
|
||||
import { BIG_INT_ZERO } from '../../constants'
|
||||
|
||||
const PageWrapper = styled(AutoColumn)`
|
||||
max-width: 640px;
|
||||
@@ -107,11 +109,24 @@ export default function Pool() {
|
||||
|
||||
const hasV1Liquidity = useUserHasLiquidityInAllTokens()
|
||||
|
||||
// show liquidity even if its deposited in rewards contract
|
||||
const stakingInfo = useStakingInfo()
|
||||
const stakingInfosWithBalance = stakingInfo?.filter(pool => JSBI.greaterThan(pool.stakedAmount.raw, BIG_INT_ZERO))
|
||||
const stakingPairs = usePairs(stakingInfosWithBalance?.map(stakingInfo => stakingInfo.tokens))
|
||||
|
||||
// remove any pairs that also are included in pairs with stake in mining pool
|
||||
const v2PairsWithoutStakedAmount = allV2PairsWithLiquidity.filter(v2Pair => {
|
||||
return (
|
||||
stakingPairs
|
||||
?.map(stakingPair => stakingPair[1])
|
||||
.filter(stakingPair => stakingPair?.liquidityToken.address === v2Pair.liquidityToken.address).length === 0
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageWrapper>
|
||||
<SwapPoolTabs active={'pool'} />
|
||||
|
||||
<VoteCard>
|
||||
<CardBGImage />
|
||||
<CardNoise />
|
||||
@@ -170,7 +185,7 @@ export default function Pool() {
|
||||
<Dots>Loading</Dots>
|
||||
</TYPE.body>
|
||||
</EmptyProposals>
|
||||
) : allV2PairsWithLiquidity?.length > 0 ? (
|
||||
) : allV2PairsWithLiquidity?.length > 0 || stakingPairs?.length > 0 ? (
|
||||
<>
|
||||
<ButtonSecondary>
|
||||
<RowBetween>
|
||||
@@ -180,10 +195,19 @@ export default function Pool() {
|
||||
<span> ↗</span>
|
||||
</RowBetween>
|
||||
</ButtonSecondary>
|
||||
|
||||
{allV2PairsWithLiquidity.map(v2Pair => (
|
||||
{v2PairsWithoutStakedAmount.map(v2Pair => (
|
||||
<FullPositionCard key={v2Pair.liquidityToken.address} pair={v2Pair} />
|
||||
))}
|
||||
{stakingPairs.map(
|
||||
(stakingPair, i) =>
|
||||
stakingPair[1] && ( // skip pairs that arent loaded
|
||||
<FullPositionCard
|
||||
key={stakingInfosWithBalance[i].stakingRewardAddress}
|
||||
pair={stakingPair[1]}
|
||||
stakedBalance={stakingInfosWithBalance[i].stakedAmount}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<EmptyProposals>
|
||||
|
||||
@@ -9,19 +9,21 @@ import { CardSection, DataCard } from '../../components/earn/styled'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { ButtonPrimary } from '../../components/Button'
|
||||
import { ProposalStatus } from './styled'
|
||||
import { useProposalData, useUserVotes, useUserDelegatee, ProposalData } from '../../state/governance/hooks'
|
||||
import { useTimestampFromBlock } from '../../hooks/useTimestampFromBlock'
|
||||
import { useProposalData, useUserVotesAsOfBlock, ProposalData, useUserDelegatee } from '../../state/governance/hooks'
|
||||
import { DateTime } from 'luxon'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import VoteModal from '../../components/vote/VoteModal'
|
||||
import { TokenAmount, JSBI } from '@uniswap/sdk'
|
||||
import { useTokenBalance } from '../../state/wallet/hooks'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { UNI, ZERO_ADDRESS, PROPOSAL_LENGTH_IN_DAYS, COMMON_CONTRACT_NAMES } from '../../constants'
|
||||
import { AVERAGE_BLOCK_TIME_IN_SECS, COMMON_CONTRACT_NAMES, UNI, ZERO_ADDRESS } from '../../constants'
|
||||
import { isAddress, getEtherscanLink } from '../../utils'
|
||||
import { ApplicationModal } from '../../state/application/actions'
|
||||
import { useModalOpen, useToggleDelegateModal, useToggleVoteModal } from '../../state/application/hooks'
|
||||
import { useModalOpen, useToggleDelegateModal, useToggleVoteModal, useBlockNumber } from '../../state/application/hooks'
|
||||
import DelegateModal from '../../components/vote/DelegateModal'
|
||||
import { GreyCard } from '../../components/Card'
|
||||
import { useTokenBalance } from '../../state/wallet/hooks'
|
||||
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
|
||||
import { BigNumber } from 'ethers'
|
||||
|
||||
const PageWrapper = styled(AutoColumn)`
|
||||
width: 100%;
|
||||
@@ -97,12 +99,16 @@ const DetailText = styled.div`
|
||||
word-break: break-all;
|
||||
`
|
||||
|
||||
const ProposerAddressLink = styled(ExternalLink)`
|
||||
word-break: break-all;
|
||||
`
|
||||
|
||||
export default function VotePage({
|
||||
match: {
|
||||
params: { id }
|
||||
}
|
||||
}: RouteComponentProps<{ id: string }>) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
|
||||
// get data for this specific proposal
|
||||
const proposalData: ProposalData | undefined = useProposalData(id)
|
||||
@@ -119,10 +125,16 @@ export default function VotePage({
|
||||
const toggelDelegateModal = useToggleDelegateModal()
|
||||
|
||||
// get and format date from data
|
||||
const startTimestamp: number | undefined = useTimestampFromBlock(proposalData?.startBlock)
|
||||
const endDate: DateTime | undefined = startTimestamp
|
||||
? DateTime.fromSeconds(startTimestamp).plus({ days: PROPOSAL_LENGTH_IN_DAYS })
|
||||
: undefined
|
||||
const currentTimestamp = useCurrentBlockTimestamp()
|
||||
const currentBlock = useBlockNumber()
|
||||
const endDate: DateTime | undefined =
|
||||
proposalData && currentTimestamp && currentBlock
|
||||
? DateTime.fromSeconds(
|
||||
currentTimestamp
|
||||
.add(BigNumber.from(AVERAGE_BLOCK_TIME_IN_SECS).mul(BigNumber.from(proposalData.endBlock - currentBlock)))
|
||||
.toNumber()
|
||||
)
|
||||
: undefined
|
||||
const now: DateTime = DateTime.local()
|
||||
|
||||
// get total votes and format percentages for UI
|
||||
@@ -132,11 +144,21 @@ export default function VotePage({
|
||||
const againstPercentage: string =
|
||||
proposalData && totalVotes ? ((proposalData.againstCount * 100) / totalVotes).toFixed(0) + '%' : '0%'
|
||||
|
||||
// show delegation option if they have have a balance, have not delegated
|
||||
const availableVotes: TokenAmount | undefined = useUserVotes()
|
||||
// only count available votes as of the proposal start block
|
||||
const availableVotes: TokenAmount | undefined = useUserVotesAsOfBlock(proposalData?.startBlock ?? undefined)
|
||||
|
||||
// only show voting if user has > 0 votes at proposal start block and proposal is active,
|
||||
const showVotingButtons =
|
||||
availableVotes &&
|
||||
JSBI.greaterThan(availableVotes.raw, JSBI.BigInt(0)) &&
|
||||
proposalData &&
|
||||
proposalData.status === 'active'
|
||||
|
||||
const uniBalance: TokenAmount | undefined = useTokenBalance(account ?? undefined, chainId ? UNI[chainId] : undefined)
|
||||
const userDelegatee: string | undefined = useUserDelegatee()
|
||||
const showUnlockVoting = Boolean(
|
||||
|
||||
// in blurb link to home page if they are able to unlock
|
||||
const showLinkForUnlock = Boolean(
|
||||
uniBalance && JSBI.notEqual(uniBalance.raw, JSBI.BigInt(0)) && userDelegatee === ZERO_ADDRESS
|
||||
)
|
||||
|
||||
@@ -171,23 +193,22 @@ export default function VotePage({
|
||||
? 'Voting ends approximately ' + (endDate && endDate.toLocaleString(DateTime.DATETIME_FULL))
|
||||
: ''}
|
||||
</TYPE.main>
|
||||
{showUnlockVoting && endDate && endDate > now && (
|
||||
<ButtonPrimary
|
||||
style={{ width: 'fit-content' }}
|
||||
padding="8px"
|
||||
borderRadius="8px"
|
||||
onClick={toggelDelegateModal}
|
||||
>
|
||||
Unlock Voting
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
</RowBetween>
|
||||
{proposalData && proposalData.status === 'active' && !showVotingButtons && (
|
||||
<GreyCard>
|
||||
<TYPE.black>
|
||||
Only UNI votes that were self delegated or delegated to another address before block{' '}
|
||||
{proposalData.startBlock} are eligible for voting.{' '}
|
||||
{showLinkForUnlock && (
|
||||
<span>
|
||||
<StyledInternalLink to="/vote">Unlock voting</StyledInternalLink> to prepare for the next proposal.
|
||||
</span>
|
||||
)}
|
||||
</TYPE.black>
|
||||
</GreyCard>
|
||||
)}
|
||||
</AutoColumn>
|
||||
{!showUnlockVoting &&
|
||||
availableVotes &&
|
||||
JSBI.greaterThan(availableVotes?.raw, JSBI.BigInt(0)) &&
|
||||
endDate &&
|
||||
endDate > now ? (
|
||||
{showVotingButtons ? (
|
||||
<RowFixed style={{ width: '100%', gap: '12px' }}>
|
||||
<ButtonPrimary
|
||||
padding="8px"
|
||||
@@ -273,11 +294,11 @@ export default function VotePage({
|
||||
</AutoColumn>
|
||||
<AutoColumn gap="md">
|
||||
<TYPE.mediumHeader fontWeight={600}>Proposer</TYPE.mediumHeader>
|
||||
<ExternalLink
|
||||
<ProposerAddressLink
|
||||
href={proposalData?.proposer && chainId ? getEtherscanLink(chainId, proposalData?.proposer, 'address') : ''}
|
||||
>
|
||||
<ReactMarkdown source={proposalData?.proposer} />
|
||||
</ExternalLink>
|
||||
</ProposerAddressLink>
|
||||
</AutoColumn>
|
||||
</ProposalInfo>
|
||||
</PageWrapper>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UNI } from './../../constants/index'
|
||||
import { UNI, PRELOADED_PROPOSALS } from './../../constants/index'
|
||||
import { TokenAmount } from '@uniswap/sdk'
|
||||
import { isAddress } from 'ethers/lib/utils'
|
||||
import { useGovernanceContract, useUniContract } from '../../hooks/useContract'
|
||||
@@ -121,10 +121,11 @@ export function useAllProposalData() {
|
||||
return Boolean(p.result) && Boolean(allProposalStates[i]?.result) && Boolean(formattedEvents[i])
|
||||
})
|
||||
.map((p, i) => {
|
||||
const description = PRELOADED_PROPOSALS.get(allProposals.length - i - 1) || formattedEvents[i].description
|
||||
const formattedProposal: ProposalData = {
|
||||
id: allProposals[i]?.result?.id.toString(),
|
||||
title: formattedEvents[i].description?.split(/# |\n/g)[1] || 'Untitled',
|
||||
description: formattedEvents[i].description?.split(/# /)[1] || 'No description.',
|
||||
title: description?.split(/# |\n/g)[1] || 'Untitled',
|
||||
description: description || 'No description.',
|
||||
proposer: allProposals[i]?.result?.proposer,
|
||||
status: enumerateProposalState(allProposalStates[i]?.result?.[0]) ?? 'Undetermined',
|
||||
forCount: parseFloat(ethers.utils.formatUnits(allProposals[i]?.result?.forVotes.toString(), 18)),
|
||||
@@ -153,6 +154,7 @@ export function useUserDelegatee(): string {
|
||||
return result?.[0] ?? undefined
|
||||
}
|
||||
|
||||
// gets the users current votes
|
||||
export function useUserVotes(): TokenAmount | undefined {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const uniContract = useUniContract()
|
||||
@@ -163,6 +165,18 @@ export function useUserVotes(): TokenAmount | undefined {
|
||||
return votes && uni ? new TokenAmount(uni, votes) : undefined
|
||||
}
|
||||
|
||||
// fetch available votes as of block (usually proposal start block)
|
||||
export function useUserVotesAsOfBlock(block: number | undefined): TokenAmount | undefined {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const uniContract = useUniContract()
|
||||
|
||||
// check for available votes
|
||||
const uni = chainId ? UNI[chainId] : undefined
|
||||
const votes = useSingleCallResult(uniContract, 'getPriorVotes', [account ?? undefined, block ?? undefined])
|
||||
?.result?.[0]
|
||||
return votes && uni ? new TokenAmount(uni, votes) : undefined
|
||||
}
|
||||
|
||||
export function useDelegateCallback(): (delegatee: string | undefined) => undefined | Promise<string> {
|
||||
const { account, chainId, library } = useActiveWeb3React()
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
@@ -13,6 +13,9 @@ export default function Updater(): null {
|
||||
const { library } = useActiveWeb3React()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
const selectedListUrl = useSelector<AppState, AppState['lists']['selectedListUrl']>(
|
||||
state => state.lists.selectedListUrl
|
||||
)
|
||||
|
||||
const isWindowVisible = useIsWindowVisible()
|
||||
|
||||
@@ -54,19 +57,21 @@ export default function Updater(): null {
|
||||
// automatically update minor/patch as long as bump matches the min update
|
||||
if (bump >= min) {
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
dispatch(
|
||||
addPopup({
|
||||
key: listUrl,
|
||||
content: {
|
||||
listUpdate: {
|
||||
listUrl,
|
||||
oldList: list.current,
|
||||
newList: list.pendingUpdate,
|
||||
auto: true
|
||||
if (listUrl === selectedListUrl) {
|
||||
dispatch(
|
||||
addPopup({
|
||||
key: listUrl,
|
||||
content: {
|
||||
listUpdate: {
|
||||
listUrl,
|
||||
oldList: list.current,
|
||||
newList: list.pendingUpdate,
|
||||
auto: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
`List at url ${listUrl} could not automatically update because the version bump was only PATCH/MINOR while the update had breaking changes and should have been MAJOR`
|
||||
@@ -75,24 +80,26 @@ export default function Updater(): null {
|
||||
break
|
||||
|
||||
case VersionUpgrade.MAJOR:
|
||||
dispatch(
|
||||
addPopup({
|
||||
key: listUrl,
|
||||
content: {
|
||||
listUpdate: {
|
||||
listUrl,
|
||||
auto: false,
|
||||
oldList: list.current,
|
||||
newList: list.pendingUpdate
|
||||
}
|
||||
},
|
||||
removeAfterMs: null
|
||||
})
|
||||
)
|
||||
if (listUrl === selectedListUrl) {
|
||||
dispatch(
|
||||
addPopup({
|
||||
key: listUrl,
|
||||
content: {
|
||||
listUpdate: {
|
||||
listUrl,
|
||||
auto: false,
|
||||
oldList: list.current,
|
||||
newList: list.pendingUpdate
|
||||
}
|
||||
},
|
||||
removeAfterMs: null
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [dispatch, lists])
|
||||
}, [dispatch, lists, selectedListUrl])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { STAKING_REWARDS_INTERFACE } from '../../constants/abis/staking-rewards'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { NEVER_RELOAD, useMultipleContractSingleData } from '../multicall/hooks'
|
||||
import { tryParseAmount } from '../swap/hooks'
|
||||
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
|
||||
|
||||
export const STAKING_GENESIS = 1600387200
|
||||
|
||||
@@ -55,6 +56,8 @@ export interface StakingInfo {
|
||||
rewardRate: TokenAmount
|
||||
// when the period ends
|
||||
periodFinish: Date | undefined
|
||||
// if pool is active
|
||||
active: boolean
|
||||
// calculates a hypothetical amount of token distributed to the active account per second.
|
||||
getHypotheticalRewardRate: (
|
||||
stakedAmount: TokenAmount,
|
||||
@@ -67,6 +70,9 @@ export interface StakingInfo {
|
||||
export function useStakingInfo(pairToFilterBy?: Pair | null): StakingInfo[] {
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
|
||||
// detect if staking is ended
|
||||
const currentBlockTimestamp = useCurrentBlockTimestamp()
|
||||
|
||||
const info = useMemo(
|
||||
() =>
|
||||
chainId
|
||||
@@ -170,7 +176,12 @@ export function useStakingInfo(pairToFilterBy?: Pair | null): StakingInfo[] {
|
||||
|
||||
const individualRewardRate = getHypotheticalRewardRate(stakedAmount, totalStakedAmount, totalRewardRate)
|
||||
|
||||
const periodFinishMs = periodFinishState.result?.[0]?.mul(1000)?.toNumber()
|
||||
const periodFinishSeconds = periodFinishState.result?.[0]?.toNumber()
|
||||
const periodFinishMs = periodFinishSeconds * 1000
|
||||
|
||||
// compare period end timestamp vs current block timestamp (in seconds)
|
||||
const active =
|
||||
periodFinishSeconds && currentBlockTimestamp ? periodFinishSeconds > currentBlockTimestamp.toNumber() : true
|
||||
|
||||
memo.push({
|
||||
stakingRewardAddress: rewardsAddress,
|
||||
@@ -181,12 +192,24 @@ export function useStakingInfo(pairToFilterBy?: Pair | null): StakingInfo[] {
|
||||
totalRewardRate: totalRewardRate,
|
||||
stakedAmount: stakedAmount,
|
||||
totalStakedAmount: totalStakedAmount,
|
||||
getHypotheticalRewardRate
|
||||
getHypotheticalRewardRate,
|
||||
active
|
||||
})
|
||||
}
|
||||
return memo
|
||||
}, [])
|
||||
}, [balances, chainId, earnedAmounts, info, periodFinishes, rewardRates, rewardsAddresses, totalSupplies, uni])
|
||||
}, [
|
||||
balances,
|
||||
chainId,
|
||||
currentBlockTimestamp,
|
||||
earnedAmounts,
|
||||
info,
|
||||
periodFinishes,
|
||||
rewardRates,
|
||||
rewardsAddresses,
|
||||
totalSupplies,
|
||||
uni
|
||||
])
|
||||
}
|
||||
|
||||
export function useTotalUniEarned(): TokenAmount | undefined {
|
||||
|
||||
@@ -185,3 +185,16 @@ export const HideSmall = styled.span`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
export const HideExtraSmall = styled.span`
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
export const ExtraSmallOnly = styled.span`
|
||||
display: none;
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
display: block;
|
||||
`};
|
||||
`
|
||||
|
||||
@@ -10,5 +10,10 @@ describe('parseENSAddress', () => {
|
||||
expect(parseENSAddress('abso.lutely.eth')).toEqual({ ensName: 'abso.lutely.eth', ensPath: undefined })
|
||||
expect(parseENSAddress('eth')).toEqual(undefined)
|
||||
expect(parseENSAddress('eth/hello-world')).toEqual(undefined)
|
||||
expect(parseENSAddress('hello-world.eth')).toEqual({ ensName: 'hello-world.eth', ensPath: undefined })
|
||||
expect(parseENSAddress('-prefix-dash.eth')).toEqual(undefined)
|
||||
expect(parseENSAddress('suffix-dash-.eth')).toEqual(undefined)
|
||||
expect(parseENSAddress('it.eth')).toEqual({ ensName: 'it.eth', ensPath: undefined })
|
||||
expect(parseENSAddress('only-single--dash.eth')).toEqual(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const ENS_NAME_REGEX = /^(([a-zA-Z0-9]+\.)+)eth(\/.*)?$/
|
||||
const ENS_NAME_REGEX = /^(([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+)eth(\/.*)?$/
|
||||
|
||||
export function parseENSAddress(ensAddress: string): { ensName: string; ensPath: string | undefined } | undefined {
|
||||
const match = ensAddress.match(ENS_NAME_REGEX)
|
||||
if (!match) return undefined
|
||||
return { ensName: `${match[1].toLowerCase()}eth`, ensPath: match[3] }
|
||||
return { ensName: `${match[1].toLowerCase()}eth`, ensPath: match[4] }
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"jsx": "preserve",
|
||||
"downlevelIteration": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": ["react-spring", "jest"]
|
||||
"types": ["react-spring", "jest"],
|
||||
"baseUrl": "src"
|
||||
},
|
||||
"exclude": ["node_modules", "cypress"],
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx", "src/components/Confetti/index.js"]
|
||||
|
||||
@@ -2726,10 +2726,10 @@
|
||||
tiny-warning "^1.0.3"
|
||||
toformat "^2.0.0"
|
||||
|
||||
"@uniswap/token-lists@^1.0.0-beta.17":
|
||||
version "1.0.0-beta.17"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.17.tgz#a861fe96a0f3de91b01eae05dec05a6a2018b38e"
|
||||
integrity sha512-UVRmSsP/ghJ7Dg8BHYjAZmZL96jHlZKrFoIEuKD3g1E4FbahfwS2j2KAaf6iQuLx6RWo8PQmDZ99rfK6T2xi8w==
|
||||
"@uniswap/token-lists@^1.0.0-beta.19":
|
||||
version "1.0.0-beta.19"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.19.tgz#5256db144fba721a6233f43b92ffb388cbd58327"
|
||||
integrity sha512-19V3KM7DAe40blWW1ApiaSYwqbq0JTKMO3yChGBrXzQBl+BoQZRTNZ4waCyoZ5QM45Q0Mxd6bCn6jXcH9G1kjg==
|
||||
|
||||
"@uniswap/v2-core@1.0.0":
|
||||
version "1.0.0"
|
||||
|
||||
Reference in New Issue
Block a user