From 9cc423c82ce21cbaadb884d6db82af4d287237c0 Mon Sep 17 00:00:00 2001 From: Ben Lyaunzon Date: Thu, 1 Jul 2021 13:12:28 -0700 Subject: [PATCH 01/29] save prog --- package.json | 1 + ts/components/staking/staking_calculator.tsx | 2 +- ts/pages/account/dashboard.tsx | 13 +++++++- yarn.lock | 31 +++++++++++++++++++- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 938249148..f1c0ef7a3 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "moment": "2.21.0", "moment-precise-range-plugin": "^1.3.0", "moment-timezone": "^0.5.33", + "nice-color-palettes": "^3.0.0", "numeral": "^2.0.6", "polished": "^1.9.2", "query-string": "^6.0.0", diff --git a/ts/components/staking/staking_calculator.tsx b/ts/components/staking/staking_calculator.tsx index cc1634046..40ff667c2 100644 --- a/ts/components/staking/staking_calculator.tsx +++ b/ts/components/staking/staking_calculator.tsx @@ -135,7 +135,7 @@ const ZRXLabel = styled.label` margin-right: 20px; `; -const ZRXInput: React.FC = ({ value, className, placeholder, onChange }) => ( +export const ZRXInput: React.FC = ({ value, className, placeholder, onChange }) => ( ZRX diff --git a/ts/pages/account/dashboard.tsx b/ts/pages/account/dashboard.tsx index eaa4cb40f..84949957b 100644 --- a/ts/pages/account/dashboard.tsx +++ b/ts/pages/account/dashboard.tsx @@ -14,6 +14,7 @@ import { Loading } from 'ts/components/portal/loading'; import { ChangePoolDialog } from 'ts/components/staking/change_pool_dialog'; import { StakingPageLayout } from 'ts/components/staking/layout/staking_page_layout'; import { RemoveStakeDialog } from 'ts/components/staking/remove_stake_dialog'; +import { StakeRebalance } from 'ts/components/staking/stake_rebalance'; import { Heading, Paragraph } from 'ts/components/text'; import { InfoTooltip } from 'ts/components/ui/info_tooltip'; import { StatFigure } from 'ts/components/ui/stat_figure'; @@ -37,6 +38,7 @@ import { colors } from 'ts/style/colors'; import { AccountReady, EpochWithFees, + PoolEpochDelegatorStats, PoolWithStats, StakeStatus, StakingAPIDelegatorResponse, @@ -449,6 +451,14 @@ export const Account: React.FC = () => { } const nextEpochStart = nextEpochStats && new Date(nextEpochStats.epochStart.timestamp); + const poolData = delegatorData.forCurrentEpoch.poolData.map((delegatorPoolStats: PoolEpochDelegatorStats) => { + const poolId = delegatorPoolStats.poolId; + const pool = poolWithStatsMap[poolId]; + return { pool, zrxStaked: delegatorPoolStats.zrxStaked }; + }); + + console.log(delegatorData); + return ( @@ -803,7 +813,6 @@ export const Account: React.FC = () => { setStakingError(undefined); }} /> - = () => { )} + + {}} poolData={poolData} stakingPools={stakingPools || []} /> ); }; diff --git a/yarn.lock b/yarn.lock index 395e44da0..df0bf6887 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9302,7 +9302,7 @@ got@7.1.0, got@^7.1.0: url-parse-lax "^1.0.0" url-to-options "^1.0.1" -got@9.6.0: +got@9.6.0, got@^9.2.2: version "9.6.0" resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== @@ -11898,6 +11898,13 @@ map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" +map-limit@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/map-limit/-/map-limit-0.0.1.tgz#eb7961031c0f0e8d001bf2d56fab685d58822f38" + integrity sha1-63lhAxwPDo0AG/LVb6toXViCLzg= + dependencies: + once "~1.3.0" + map-obj@^1.0.0, map-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" @@ -12586,10 +12593,25 @@ neo-async@^2.6.0, neo-async@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" +new-array@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/new-array/-/new-array-1.0.0.tgz#5dbc639d961eac7f1a9fbc1a7146ec12f2924fbf" + integrity sha1-XbxjnZYerH8an7wacUbsEvKST78= + next-tick@1, next-tick@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" +nice-color-palettes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/nice-color-palettes/-/nice-color-palettes-3.0.0.tgz#1ec31927cfc4ce8a51822b6bab2c64845d181abb" + integrity sha512-lL4AjabAAFi313tjrtmgm/bxCRzp4l3vCshojfV/ij3IPdtnRqv6Chcw+SqJUhbe7g3o3BecaqCJYUNLswGBhQ== + dependencies: + got "^9.2.2" + map-limit "0.0.1" + minimist "^1.2.0" + new-array "^1.0.0" + nice-try@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4" @@ -13019,6 +13041,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +once@~1.3.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20" + integrity sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA= + dependencies: + wrappy "1" + onetime@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" From bb34f823474d24af0181048f7871e2b247bb05ab Mon Sep 17 00:00:00 2001 From: Ben Lyaunzon Date: Thu, 1 Jul 2021 13:19:38 -0700 Subject: [PATCH 02/29] saving prog --- ts/components/slider/percentage_slider.tsx | 212 ++++++++++++++ ts/components/staking/add_pool_dialog.tsx | 230 +++++++++++++++ ts/components/staking/stake_rebalance.tsx | 323 +++++++++++++++++++++ 3 files changed, 765 insertions(+) create mode 100644 ts/components/slider/percentage_slider.tsx create mode 100644 ts/components/staking/add_pool_dialog.tsx create mode 100644 ts/components/staking/stake_rebalance.tsx diff --git a/ts/components/slider/percentage_slider.tsx b/ts/components/slider/percentage_slider.tsx new file mode 100644 index 000000000..7cc548e4d --- /dev/null +++ b/ts/components/slider/percentage_slider.tsx @@ -0,0 +1,212 @@ +import * as React from 'react'; + +const colors = require('nice-color-palettes'); +import { PoolWithStats } from 'ts/types'; + +import styled from 'styled-components'; + +const PercentageSliderWrapper = styled.div` + .tag:last-of-type > .slider-button { + display: none !important; + } +`; + +const randPalette = colors + .sort(function () { + return 0.5 - Math.random(); + }) + .pop(); + +const _tags = [ + { + name: 'Action', + color: 'red', + }, + { + name: 'Romance', + color: 'purple', + }, + { + name: 'Comedy', + color: 'orange', + }, + { + name: 'Horror', + color: 'black', + }, +]; + +const getPercentage = (containerWidth: number, distanceMoved: number) => { + return (distanceMoved / containerWidth) * 100; +}; + +const limitNumberWithinRange = (value: number, min: number, max: number): number => { + return Math.min(Math.max(value, min), max); +}; + +const nearestN = (N: number, number: number) => Math.ceil(number / N) * N; +interface TagSectionProps { + name: string; + color: string; + width: number; + onSliderSelect: (e: React.MouseEvent) => void; +} + +const TagSection = ({ name, color, width, onSliderSelect }: TagSectionProps) => { + return ( +
+ {name} + {Math.round(width) + '%'} + +
+ +
+
+ ); +}; + +interface PoolDataElement { + pool: PoolWithStats; + zrxStaked: number; +} + +interface PercentageSliderProps { + pools: PoolDataElement[]; + tags: Tag[]; + widths: number[]; + setWidths: React.Dispatch>; +} + +interface Tag { + name: string; + color: string; +} + +export const PercentageSlider: React.FC = ({ pools, tags, widths, setWidths }) => { + // const [widths, setWidths] = React.useState(new Array(poolTags.length).fill(100 / poolTags.length)); + // const [tags, setTags] = React.useState(poolTags); + const TagSliderRef = React.useRef(null); + return ( + +
+ {tags.map((tag, index) => ( + { + e.preventDefault(); + document.body.style.cursor = 'ew-resize'; + + const startDragX = e.pageX; + const sliderWidth = TagSliderRef.current.offsetWidth; + + const resize = (e: MouseEvent & TouchEvent & PointerEvent) => { + e.preventDefault(); + const endDragX = e.touches ? e.touches[0].pageX : e.pageX; + const distanceMoved = endDragX - startDragX; + const maxPercent = widths[index] + widths[index + 1]; + + const percentageMoved = nearestN(1, getPercentage(sliderWidth, distanceMoved)); + // const percentageMoved = getPercentage(sliderWidth, distanceMoved); + + const _widths = widths.slice(); + + const prevPercentage = _widths[index]; + + const newPercentage = prevPercentage + percentageMoved; + const currentSectionWidth = limitNumberWithinRange(newPercentage, 0, maxPercent); + _widths[index] = currentSectionWidth; + + const nextSectionIndex = index + 1; + + const nextSectionNewPercentage = _widths[nextSectionIndex] - percentageMoved; + const nextSectionWidth = limitNumberWithinRange( + nextSectionNewPercentage, + 0, + maxPercent, + ); + _widths[nextSectionIndex] = nextSectionWidth; + + setWidths(_widths); + }; + + window.addEventListener('pointermove', resize); + window.addEventListener('touchmove', resize); + + const removeEventListener = () => { + window.removeEventListener('pointermove', resize); + window.removeEventListener('touchmove', resize); + }; + + const handleEventUp = (e: Event) => { + e.preventDefault(); + document.body.style.cursor = 'initial'; + removeEventListener(); + }; + + window.addEventListener('touchend', handleEventUp); + window.addEventListener('pointerup', handleEventUp); + }} + color={tag.color} + /> + ))} +
+
+ ); +}; + +type StylesType = { [key: string]: React.CSSProperties }; + +const styles: StylesType = { + tag: { + padding: 20, + textAlign: 'center', + position: 'relative', + borderRightWidth: '.1em', + borderRightStyle: 'solid', + borderRightColor: 'white', + boxSizing: 'border-box', + borderLeftWidth: '.1em', + borderLeftStyle: 'solid', + borderLeftColor: 'white', + }, + tagText: { + color: 'white', + fontWeight: 700, + userSelect: 'none', + display: 'block', + overflow: 'hidden', + }, + sliderButton: { + width: '2em', + height: '2em', + backgroundColor: 'white', + position: 'absolute', + borderRadius: '2em', + right: 'calc(-1.1em)', + top: 0, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + bottom: 0, + margin: 'auto', + zIndex: 10, + cursor: 'ew-resize', + userSelect: 'none', + }, +}; diff --git a/ts/components/staking/add_pool_dialog.tsx b/ts/components/staking/add_pool_dialog.tsx new file mode 100644 index 000000000..f75493814 --- /dev/null +++ b/ts/components/staking/add_pool_dialog.tsx @@ -0,0 +1,230 @@ +import { BigNumber } from '@0x/utils'; +import { DialogContent, DialogOverlay } from '@reach/dialog'; +import '@reach/dialog/styles.css'; +import React, { FC, useMemo, useState } from 'react'; +import styled from 'styled-components'; + +import { Button } from 'ts/components/button'; +import { Icon } from 'ts/components/icon'; +import { Input } from 'ts/components/modals/input'; +import { Heading, Paragraph } from 'ts/components/text'; +import { Thumbnail } from 'ts/components/staking/thumbnail.tsx'; + +import { useSearch } from 'ts/hooks/use_search'; +import { zIndex } from 'ts/style/z_index'; +import { PoolWithStats } from 'ts/types'; +import { colors } from 'ts/utils/colors'; +import { stakingUtils } from 'ts/utils/staking_utils'; + +interface ChangePoolDialogProps { + isOpen: boolean; + onDismiss: () => void; + onAddPool: (poolId: string) => void; + stakingPools: PoolWithStats[]; +} + +interface PoolWithDisplayName extends PoolWithStats { + displayName: string; +} + +const searchOptions = { + keys: ['displayName'], +}; + +export const AddPoolDialog: FC = ({ isOpen, onDismiss, stakingPools, onAddPool }) => { + const stakingPoolsWithName: PoolWithDisplayName[] = useMemo( + () => + stakingPools.sort(stakingUtils.sortByProtocolFeesDesc).map((pool) => ({ + ...pool, + displayName: stakingUtils.getPoolDisplayName(pool), + })), + [stakingPools], + ); + + const { setSearchTerm, searchResults } = useSearch(stakingPoolsWithName, searchOptions); + + const [selectedPoolId, setSelectedPoolId] = useState(undefined); + + const clearAndDismiss = () => { + setSelectedPoolId(undefined); + setSearchTerm(''); + onDismiss(); + }; + + return ( + + + + + + + <> + Select a pool + Choose a liquidity pool from the list to rebalance into. + + + + setSearchTerm(e.target.value)} + /> + + + + {(searchResults.length ? searchResults : stakingPools).map((pool) => ( + setSelectedPoolId(pool.poolId)} + isSelected={pool.poolId === selectedPoolId} + > + + {stakingUtils.getPoolDisplayName(pool)} + + ))} + + + { + onAddPool(selectedPoolId); + clearAndDismiss(); + }} + > + Choose the selected pool + + + + + + ); +}; + +const ButtonWrapper = styled.div` + margin-top: 33px; + display: flex; + justify-content: flex-end; + + @media (max-width: 768px) { + justify-content: center; + } +`; + +const StyledParagraph = styled(Paragraph)` + font-style: normal; + font-weight: 300; + font-size: 18px; + line-height: 26px; +`; + +const StyledHeading = styled(Heading)` + font-size: 34px; + line-height: 42px; + margin-bottom: 20px; + + font-feature-settings: 'tnum' on, 'lnum' on; +`; + +const StyledInput = styled(Input)` + input { + ::placeholder { + color: ${(props) => props.theme.textDarkSecondary}; + } + + background-color: ${(props) => props.theme.lightBgColor}; + border: none; + } +`; + +const InputWrapper = styled.div` + display: flex; + align-items: baseline; + + @media (min-width: 768px) { + box-shadow: 0px 1px 0px #b4bebd; + margin-bottom: 23px; + } +`; + +const PoolsListWrapper = styled.div` + overflow-y: scroll; + @media (min-width: 768px) { + height: 500px; + max-height: 40vh; + } +`; + +const ConfirmButton = styled(Button)` + background-color: ${(props) => props.isDisabled && '#898990'}; + cursor: ${(props) => (props.isDisabled ? 'not-allowed' : 'pointer')}; + color: #fff; + &:hover { + background-color: ${(props) => props.isDisabled && '#898990'}; + } +`; + +const ButtonClose = styled(Button)` + width: 18px; + height: 18px; + border: none; + + align-self: flex-end; + + path { + fill: ${colors.black}; + } +`; + +const Pool = styled.div<{ isSelected?: boolean }>` + height: 87px; + background: #fff; + border: 1px solid ${(props) => (props.isSelected ? '#00AE99' : '#ddd')}; + display: flex; + flex-direction: row; + align-items: center; + cursor: pointer; + + & + & { + margin-top: 14px; + } +`; + +const StyledDialogOverlay = styled(DialogOverlay)` + &[data-reach-dialog-overlay] { + background-color: rgba(255, 255, 255, 0.8); + z-index: ${zIndex.overlay}; + + @media (min-width: 768px) { + overflow: hidden; + } + } +`; + +const StyledDialogContent = styled(DialogContent)` + &[data-reach-dialog-content] { + display: flex; + flex-direction: column; + position: relative; + width: 600px; + background: ${(props) => props.theme.lightBgColor}; + border: 1px solid #e5e5e5; + + @media (max-width: 768px) { + min-height: 100vh; + width: 100vw; + margin: 0; + padding: 30px; + + border: none; + } + } +`; + +const StyledThumbnail = styled(Thumbnail)` + margin: 0 20px; +`; diff --git a/ts/components/staking/stake_rebalance.tsx b/ts/components/staking/stake_rebalance.tsx new file mode 100644 index 000000000..3ba42900b --- /dev/null +++ b/ts/components/staking/stake_rebalance.tsx @@ -0,0 +1,323 @@ +import * as React from 'react'; + +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; +import { State } from 'ts/redux/reducer'; +import { stakingUtils } from 'ts/utils/staking_utils'; + +import { Button } from 'ts/components/button'; +import { Icon } from 'ts/components/icon'; +import { AddPoolDialog } from 'ts/components/staking/add_pool_dialog'; +import { Loading } from 'ts/components/portal/loading'; +import { ZRXInput } from 'ts/components/staking/staking_calculator'; +import { PercentageSlider } from 'ts/components/slider/percentage_slider'; + +import { Heading } from 'ts/components/text'; +import { Select, SelectItemConfig } from 'ts/components/ui/select'; +import { useAPIClient } from 'ts/hooks/use_api_client'; + +import { colors } from 'ts/style/colors'; +import { PoolEpochDelegatorStats, PoolWithStats } from 'ts/types'; + +const paletteColors = require('nice-color-palettes'); +const randPalette = paletteColors + .sort(function () { + return 0.5 - Math.random(); + }) + + .pop(); +const ButtonClose = styled(Button)` + width: 22px; + height: 22px; + border: none; + align-self: flex-end; + path { + fill: ${colors.textDarkSecondary}; + } +`; + +const AddPoolButton = styled(Button)` + margin-bottom: 20px; +`; +const Container = styled.div` + position: fixed; + right: 0; + top: 0; + background-color: #f6f6f6; + width: 500px; + padding: 2rem; + height: 100%; + z-index: 2; + box-shadow: 10px 0px 20px #000000; + display: flex; + flex-direction: column; +`; + +const StakingPoolsContainer = styled.div` + margin: 1rem 0; +`; + +const StakingPoolWrapper = styled.div` + margin-bottom: 1rem; +`; + +const ButtonsContainer = styled.div` + margin-top: 2rem; +`; +const LoadingContainer = styled.div` + display: flex; + height: 100%; + align-items: center; + justify-content: center; +`; + +const StakingPoolLabel = styled.div` + margin-bottom: 0.5rem; +`; +const HeadingWrapper = styled.div` + margin: 1rem 0; + font-weight: 400; +`; +const YourStakeContainer = styled.div` + padding: 1rem 0; +`; +const YourStakeLabels = styled.div` + display: flex; + justify-content: space-between; + margin-bottom: 0.5rem; +`; +const YourStakeTitle = styled.div``; +const YourStakeBalance = styled.div` + color: #999999; + font-size: 14px; + display: flex; + align-items: flex-end; + cursor: pointer; +`; + +const RewardRow = styled.div` + display: flex; + justify-content: space-between; + font-size: 16px; + padding-bottom: 0.75rem; + margin-bottom: 0.85rem; +`; +const Results = styled.div` + flex: 1; + justify-content: flex-end; + display: flex; + flex-direction: column; +`; +const ResultsLabel = styled.div` + padding-bottom: 0.75rem; + margin-bottom: 0.75rem; + border-bottom: 1px solid #d7d7d7; +`; + +const PoolReward = styled(RewardRow)` + color: #999999; + + border-bottom: 1px solid #d7d7d7; +`; +const StakersReward = styled(RewardRow)` + color: #999999; + + border-bottom: 1px solid #d7d7d7; +`; +const YourReward = styled(RewardRow)` + padding-bottom: 0.75rem; + margin-bottom: 0; +`; +const YearlyReturn = styled(RewardRow)` + padding-bottom: 0.75rem; + margin-bottom: 0; +`; + +const PoolRewardResults = styled.div``; +const StakersRewardResults = styled.div``; +const YourRewardResults = styled.div` + font-weight: bolder; +`; +const YearlyReturnResults = styled.div``; + +const ZRXInputField = styled.div` + display: flex; + justify-content: space-between; + position: relative; +`; +const ZRXLabel = styled.label` + position: absolute; + right: 0; + align-self: center; + margin-right: 20px; +`; + +const Input = styled.input` + background-color: ${colors.white}; + color: ${colors.textDarkPrimary}; + border: 1px solid #d7d7d7; + width: 100%; + font-size: 18px; + padding: 16px 20px 18px; + outline: none; + &::placeholder { + color: #333333; + opacity: 0.5; + } +`; + +export interface InputProps { + className?: string; + value?: string; + width?: string; + fontSize?: string; + fontColor?: string; + padding?: string; + placeholderColor?: string; + placeholder?: string; + backgroundColor?: string; + onChange?: (event: React.ChangeEvent) => void; +} + +interface PoolDataElement { + pool: PoolWithStats; + zrxStaked: number; +} + +interface StakeRebalanceProps { + onClose: () => void; + poolData: PoolDataElement[]; + stakingPools: PoolWithStats[]; +} +export const StakeRebalance: React.FC = ({ onClose, poolData, stakingPools }) => { + const networkId = useSelector((state: State) => state.networkId); + const apiClient = useAPIClient(networkId); + + const [sliderPercentages, setSliderPercentages] = React.useState( + new Array(poolData.length).fill(100 / poolData.length), + ); + const [isAddPoolDialogOpen, setIsAddPoolDialogOpen] = React.useState(false); + const [currentPoolData, setCurrentPoolData] = React.useState(poolData); + + const node = React.useRef(); + + const handleClick = (ev: MouseEvent): any => { + if (node.current.contains(ev.target as Element)) { + return; + } + onClose(); + }; + + React.useEffect(() => { + document.addEventListener('mousedown', handleClick); + + return () => { + document.removeEventListener('mousedown', handleClick); + }; + }, []); + + const updateSliderPercentage = (widths: number[]) => { + setSliderPercentages(widths); + const sumOfZrx = currentPoolData + .map((item) => item.zrxStaked) + .reduce((accumulator, currentValue) => accumulator + currentValue); + const updatedCurrentPoolData = currentPoolData.map((item) => { + return { + pool: item.pool, + zrxStaked: item.zrxStaked, + }; + }); + }; + + const addPool = (poolId: string) => { + const pool = stakingPools.find((pool) => { + return pool.poolId === poolId; + }); + const updatedCurrentPoolData = [ + ...currentPoolData, + { + pool, + zrxStaked: 0, + }, + ]; + setCurrentPoolData(updatedCurrentPoolData); + setSliderPercentages(new Array(updatedCurrentPoolData.length).fill(100 / updatedCurrentPoolData.length)); + }; + + const filteredStakingPools = stakingPools.filter((pool) => { + const foundPool = currentPoolData.find((poolDataPool) => { + return poolDataPool.pool.poolId === pool.poolId; + }); + return !foundPool; + }); + + const poolTags = currentPoolData.map((pool, index) => { + return { + name: pool.pool.poolId, + color: randPalette[index], + }; + }); + + return ( + <> + + + + + {true && ( + <> + + + Change your Stake + + + + {currentPoolData.map((data) => { + return ( + + + {stakingUtils.getPoolDisplayName(data.pool)} + + + + ); + })} + + + + { + setIsAddPoolDialogOpen(true); + }} + > + + Add Pool + + + + { + setIsAddPoolDialogOpen(false); + }} + onAddPool={addPool} + /> + + )} + + + ); +}; From 515429483c15d783dddbcdd1ab8541711c689d4e Mon Sep 17 00:00:00 2001 From: Ben Lyaunzon Date: Mon, 5 Jul 2021 18:55:15 -0700 Subject: [PATCH 03/29] saving progress --- ts/components/slider/percentage_slider.tsx | 17 +- ts/components/staking/stake_rebalance.tsx | 345 ++++++++++++++------- ts/hooks/use_stake.ts | 40 +++ ts/pages/account/dashboard.tsx | 38 ++- 4 files changed, 306 insertions(+), 134 deletions(-) diff --git a/ts/components/slider/percentage_slider.tsx b/ts/components/slider/percentage_slider.tsx index 7cc548e4d..ddb711860 100644 --- a/ts/components/slider/percentage_slider.tsx +++ b/ts/components/slider/percentage_slider.tsx @@ -62,12 +62,9 @@ const TagSection = ({ name, color, width, onSliderSelect }: TagSectionProps) => width: width + '%', }} > - {name} - {Math.round(width) + '%'} + {Math.round(width) + '%'} -
- -
+
); }; @@ -81,7 +78,7 @@ interface PercentageSliderProps { pools: PoolDataElement[]; tags: Tag[]; widths: number[]; - setWidths: React.Dispatch>; + setWidths: (widths: number[]) => void; } interface Tag { @@ -174,7 +171,7 @@ type StylesType = { [key: string]: React.CSSProperties }; const styles: StylesType = { tag: { - padding: 20, + padding: 10, textAlign: 'center', position: 'relative', borderRightWidth: '.1em', @@ -193,12 +190,12 @@ const styles: StylesType = { overflow: 'hidden', }, sliderButton: { - width: '2em', - height: '2em', + width: '1em', + height: '1em', backgroundColor: 'white', position: 'absolute', borderRadius: '2em', - right: 'calc(-1.1em)', + right: 'calc(-.6em)', top: 0, display: 'flex', justifyContent: 'center', diff --git a/ts/components/staking/stake_rebalance.tsx b/ts/components/staking/stake_rebalance.tsx index 3ba42900b..564c55479 100644 --- a/ts/components/staking/stake_rebalance.tsx +++ b/ts/components/staking/stake_rebalance.tsx @@ -1,7 +1,11 @@ import * as React from 'react'; +import * as _ from 'lodash'; + import { useSelector } from 'react-redux'; import styled from 'styled-components'; + +import { MoveStakeData } from 'ts/hooks/use_stake'; import { State } from 'ts/redux/reducer'; import { stakingUtils } from 'ts/utils/staking_utils'; @@ -20,12 +24,8 @@ import { colors } from 'ts/style/colors'; import { PoolEpochDelegatorStats, PoolWithStats } from 'ts/types'; const paletteColors = require('nice-color-palettes'); -const randPalette = paletteColors - .sort(function () { - return 0.5 - Math.random(); - }) +const randPalette = ['#fe4365', '#fc9d9a', '#f9cdad', '#c8c8a9', '#83af9b']; - .pop(); const ButtonClose = styled(Button)` width: 22px; height: 22px; @@ -63,12 +63,10 @@ const StakingPoolWrapper = styled.div` const ButtonsContainer = styled.div` margin-top: 2rem; -`; -const LoadingContainer = styled.div` + flex: 1; display: flex; - height: 100%; - align-items: center; - justify-content: center; + flex-direction: column; + justify-content: flex-end; `; const StakingPoolLabel = styled.div` @@ -78,22 +76,6 @@ const HeadingWrapper = styled.div` margin: 1rem 0; font-weight: 400; `; -const YourStakeContainer = styled.div` - padding: 1rem 0; -`; -const YourStakeLabels = styled.div` - display: flex; - justify-content: space-between; - margin-bottom: 0.5rem; -`; -const YourStakeTitle = styled.div``; -const YourStakeBalance = styled.div` - color: #999999; - font-size: 14px; - display: flex; - align-items: flex-end; - cursor: pointer; -`; const RewardRow = styled.div` display: flex; @@ -102,68 +84,27 @@ const RewardRow = styled.div` padding-bottom: 0.75rem; margin-bottom: 0.85rem; `; -const Results = styled.div` - flex: 1; - justify-content: flex-end; - display: flex; - flex-direction: column; -`; -const ResultsLabel = styled.div` - padding-bottom: 0.75rem; - margin-bottom: 0.75rem; - border-bottom: 1px solid #d7d7d7; -`; -const PoolReward = styled(RewardRow)` - color: #999999; - - border-bottom: 1px solid #d7d7d7; -`; -const StakersReward = styled(RewardRow)` - color: #999999; - - border-bottom: 1px solid #d7d7d7; -`; -const YourReward = styled(RewardRow)` - padding-bottom: 0.75rem; - margin-bottom: 0; -`; -const YearlyReturn = styled(RewardRow)` - padding-bottom: 0.75rem; - margin-bottom: 0; -`; - -const PoolRewardResults = styled.div``; -const StakersRewardResults = styled.div``; -const YourRewardResults = styled.div` - font-weight: bolder; +const ColorBox = styled.div` + width: 20px; + height: 20px; + margin-left: 0.5rem; + position: absolute; + margin-top: -0.15rem; + display: inline-block; `; -const YearlyReturnResults = styled.div``; -const ZRXInputField = styled.div` +const StakingPoolLabelWrapper = styled.div` display: flex; justify-content: space-between; - position: relative; -`; -const ZRXLabel = styled.label` - position: absolute; - right: 0; - align-self: center; - margin-right: 20px; `; -const Input = styled.input` - background-color: ${colors.white}; - color: ${colors.textDarkPrimary}; - border: 1px solid #d7d7d7; - width: 100%; - font-size: 18px; - padding: 16px 20px 18px; - outline: none; - &::placeholder { - color: #333333; - opacity: 0.5; - } +const RemovePool = styled.div` + text-decoration: underline; + cursor: pointer; + font-size: 14px; + align-self: flex-end; + margin-bottom: 0.5rem; `; export interface InputProps { @@ -184,15 +125,25 @@ interface PoolDataElement { zrxStaked: number; } +interface PoolDiff { + poolId: string; + diff: number; +} + interface StakeRebalanceProps { onClose: () => void; poolData: PoolDataElement[]; stakingPools: PoolWithStats[]; + rebalanceStake: (rebalanceStakeData: MoveStakeData[]) => void; } -export const StakeRebalance: React.FC = ({ onClose, poolData, stakingPools }) => { +export const StakeRebalance: React.FC = ({ onClose, poolData, stakingPools, rebalanceStake }) => { const networkId = useSelector((state: State) => state.networkId); const apiClient = useAPIClient(networkId); + const originalSumOfZrx = poolData + .map((item) => item.zrxStaked) + .reduce((accumulator, currentValue) => accumulator + currentValue); + const [sliderPercentages, setSliderPercentages] = React.useState( new Array(poolData.length).fill(100 / poolData.length), ); @@ -201,32 +152,33 @@ export const StakeRebalance: React.FC = ({ onClose, poolDat const node = React.useRef(); - const handleClick = (ev: MouseEvent): any => { - if (node.current.contains(ev.target as Element)) { - return; - } - onClose(); - }; + // const handleClick = (ev: MouseEvent): any => { + // if (node.current.contains(ev.target as Element)) { + // return; + // } + // onClose(); + // }; - React.useEffect(() => { - document.addEventListener('mousedown', handleClick); + // React.useEffect(() => { + // document.addEventListener('mousedown', handleClick); - return () => { - document.removeEventListener('mousedown', handleClick); - }; - }, []); + // return () => { + // document.removeEventListener('mousedown', handleClick); + // }; + // }, []); - const updateSliderPercentage = (widths: number[]) => { + const updateSliderPercentages = (widths: number[]) => { setSliderPercentages(widths); const sumOfZrx = currentPoolData .map((item) => item.zrxStaked) .reduce((accumulator, currentValue) => accumulator + currentValue); - const updatedCurrentPoolData = currentPoolData.map((item) => { + const updatedCurrentPoolData = currentPoolData.map((item, index) => { return { pool: item.pool, - zrxStaked: item.zrxStaked, + zrxStaked: parseFloat((sumOfZrx * (widths[index] / 100)).toFixed(2)), }; }); + setCurrentPoolData(updatedCurrentPoolData); }; const addPool = (poolId: string) => { @@ -240,8 +192,21 @@ export const StakeRebalance: React.FC = ({ onClose, poolDat zrxStaked: 0, }, ]; - setCurrentPoolData(updatedCurrentPoolData); - setSliderPercentages(new Array(updatedCurrentPoolData.length).fill(100 / updatedCurrentPoolData.length)); + const updatedSliderPercentages = new Array(updatedCurrentPoolData.length).fill( + 100 / updatedCurrentPoolData.length, + ); + + const sumOfZrx = updatedCurrentPoolData + .map((item) => item.zrxStaked) + .reduce((accumulator, currentValue) => accumulator + currentValue); + const updatedPoolDataWithStakedAmounts = updatedCurrentPoolData.map((item, index) => { + return { + pool: item.pool, + zrxStaked: parseFloat((sumOfZrx * (updatedSliderPercentages[index] / 100)).toFixed(2)), + }; + }); + setCurrentPoolData(updatedPoolDataWithStakedAmounts); + setSliderPercentages(updatedSliderPercentages); }; const filteredStakingPools = stakingPools.filter((pool) => { @@ -258,6 +223,121 @@ export const StakeRebalance: React.FC = ({ onClose, poolDat }; }); + const onPoolInputChange = (e: React.ChangeEvent, index: number) => { + const updatedStakedZrx = parseFloat(e.target.value || '0'); + let updatedCurrentPoolData = [...currentPoolData]; + if (updatedStakedZrx <= originalSumOfZrx) { + const remainderZrx = (originalSumOfZrx - updatedStakedZrx) / (updatedCurrentPoolData.length - 1); + updatedCurrentPoolData = updatedCurrentPoolData.map((item, innerIndex) => { + if (index !== innerIndex) { + return { + pool: item.pool, + zrxStaked: remainderZrx, + }; + } + return { + pool: item.pool, + zrxStaked: updatedStakedZrx, + }; + }); + } + + const sumOfZrx = updatedCurrentPoolData + .map((item) => item.zrxStaked) + .reduce((accumulator, currentValue) => accumulator + currentValue); + + const _widths = updatedCurrentPoolData.map((item) => { + return (item.zrxStaked / sumOfZrx) * 100; + }); + + setCurrentPoolData(updatedCurrentPoolData); + setSliderPercentages(_widths); + }; + + const removePool = (poolId: string) => { + const updatedCurrentPoolData = currentPoolData.filter((item) => { + return item.pool.poolId !== poolId; + }); + + const updatedSliderPercentages = new Array(updatedCurrentPoolData.length).fill( + 100 / updatedCurrentPoolData.length, + ); + + const updatedPoolDataWithStakedAmounts = updatedCurrentPoolData.map((item, index) => { + return { + pool: item.pool, + zrxStaked: parseFloat((originalSumOfZrx * (updatedSliderPercentages[index] / 100)).toFixed(2)), + }; + }); + + setCurrentPoolData(updatedPoolDataWithStakedAmounts); + setSliderPercentages(updatedSliderPercentages); + }; + + const generateMoveStakeData = (poolDiff: PoolDiff, reductions: PoolDiff[]): MoveStakeData[] | MoveStakeData => { + const { poolId, diff } = poolDiff; + let accumulatedAmount = 0; + const moveStakeData = []; + for (let index = 0; index < reductions.length; index++) { + const element = reductions[index]; + const availAmt = Math.abs(element.diff); + if (availAmt >= diff - accumulatedAmount) { + moveStakeData.push({ + fromPoolId: element.poolId, + toPoolId: poolId, + zrxAmount: parseFloat((diff - accumulatedAmount).toFixed(2)), + }); + } + accumulatedAmount += availAmt; + moveStakeData.push({ + fromPoolId: element.poolId, + toPoolId: poolId, + zrxAmount: parseFloat(availAmt.toFixed(2)), + }); + } + return moveStakeData; + }; + + const rebalanceStakeAcrossPools = () => { + const additions: PoolDiff[] = []; + const reductions: PoolDiff[] = []; + + currentPoolData.forEach((item) => { + const foundPool = poolData.find((foundItem) => { + return item.pool.poolId === foundItem.pool.poolId; + }); + let diff = 0; + if (foundPool) { + diff = item.zrxStaked - foundPool.zrxStaked; + } else { + diff = item.zrxStaked; + } + + if (diff !== 0) { + if (diff > 0) { + additions.push({ + poolId: item.pool.poolId, + diff, + }); + } else { + reductions.push({ + poolId: item.pool.poolId, + diff, + }); + } + } + }); + + console.log(additions, reductions); + + if (additions.length > 0) { + const data = _.flatMap(additions, (item) => { + return generateMoveStakeData(item, reductions); + }); + rebalanceStake(data); + } + }; + return ( <> @@ -272,13 +352,38 @@ export const StakeRebalance: React.FC = ({ onClose, poolDat - {currentPoolData.map((data) => { + {currentPoolData.map((data, index) => { + const poolTag = poolTags.find((item) => { + return item.name === data.pool.poolId; + }); + + const isStartingPool = poolData.find((item) => { + return item.pool.poolId === data.pool.poolId; + }); + return ( - - {stakingUtils.getPoolDisplayName(data.pool)} - - + + + {stakingUtils.getPoolDisplayName(data.pool)} + + + {!isStartingPool && ( + { + removePool(data.pool.poolId); + }} + > + Remove Pool + + )} + + { + onPoolInputChange(e, index); + }} + /> ); })} @@ -287,23 +392,25 @@ export const StakeRebalance: React.FC = ({ onClose, poolDat pools={currentPoolData} tags={poolTags} widths={sliderPercentages} - setWidths={setSliderPercentages} + setWidths={updateSliderPercentages} /> - { - setIsAddPoolDialogOpen(true); - }} - > - + Add Pool - - diff --git a/ts/hooks/use_stake.ts b/ts/hooks/use_stake.ts index aa1088cf9..da5681d00 100644 --- a/ts/hooks/use_stake.ts +++ b/ts/hooks/use_stake.ts @@ -44,6 +44,7 @@ export interface UseStakeHookResult { depositAndStake: (stakingPools: StakePoolData[], callback?: () => void) => void; unstake: (stakePoolData: StakePoolData[], callback?: () => void) => void; moveStake: (fromPoolId: string, toPoolId: string, zrxAmount: number, callback?: () => void) => void; + batchMoveStake: (moveStakeData: MoveStakeData[], callback?: () => void) => void; withdrawStake: (zrxAmountBaseUnits: BigNumber, callback?: () => void) => void; withdrawRewards: (poolIds: string[], callback?: () => void) => void; stakingContract?: StakingContract; @@ -55,6 +56,12 @@ export interface UseStakeHookResult { currentEpochRewards?: BigNumber; } +export interface MoveStakeData { + fromPoolId: string; + toPoolId: string; + zrxAmount: number; +} + export const useStake = (networkId: ChainId, providerState: ProviderState): UseStakeHookResult => { const [loadingState, setLoadingState] = useState(undefined); const [error, setError] = useState(undefined); @@ -213,6 +220,28 @@ export const useStake = (networkId: ChainId, providerState: ProviderState): UseS [executeWithData, stakingContract], ); + const batchMoveStakeAsync = useCallback( + async (moveStakeData: MoveStakeData[]) => { + const data = moveStakeData.map((item) => { + const { zrxAmount, fromPoolId, toPoolId } = item; + + const zrxAmountBaseUnits = toZrxBaseUnits(zrxAmount); + const fromPoolIdPadded = utils.toPaddedHex(fromPoolId); + const toPoolIdPadded = utils.toPaddedHex(toPoolId); + + return stakingContract + .moveStake( + { status: StakeStatus.Delegated, poolId: fromPoolIdPadded }, + { status: StakeStatus.Delegated, poolId: toPoolIdPadded }, + zrxAmountBaseUnits, + ) + .getABIEncodedTransactionData(); + }); + await executeWithData(data); + }, + [executeWithData, stakingContract], + ); + const withdrawStakeAsync = useCallback( async (zrxAmountBaseUnits: BigNumber) => { if (zrxAmountBaseUnits.isLessThanOrEqualTo(0) || isTxInProgress(loadingState)) { @@ -360,6 +389,17 @@ export const useStake = (networkId: ChainId, providerState: ProviderState): UseS handleError(err); }); }, + batchMoveStake: (moveStakeData: MoveStakeData[], callback?: () => void) => { + batchMoveStakeAsync(moveStakeData) + .then(() => { + trackEvent(TRACKING.MOVE_STAKE, { event_label: 'success' }); + }) + .then(callback) + .catch((err: Error) => { + trackEvent(TRACKING.MOVE_STAKE, { event_label: 'failed' }); + handleError(err); + }); + }, withdrawStake: (zrxAmountBaseUnits: BigNumber, callback?: () => void) => { withdrawStakeAsync(zrxAmountBaseUnits) .then(() => { diff --git a/ts/pages/account/dashboard.tsx b/ts/pages/account/dashboard.tsx index 84949957b..d05298ab4 100644 --- a/ts/pages/account/dashboard.tsx +++ b/ts/pages/account/dashboard.tsx @@ -19,7 +19,7 @@ import { Heading, Paragraph } from 'ts/components/text'; import { InfoTooltip } from 'ts/components/ui/info_tooltip'; import { StatFigure } from 'ts/components/ui/stat_figure'; import { useAPIClient } from 'ts/hooks/use_api_client'; -import { useStake } from 'ts/hooks/use_stake'; +import { useStake, MoveStakeData } from 'ts/hooks/use_stake'; import { AccountActivitySummary } from 'ts/pages/account/account_activity_summary'; import { AccountApplyModal } from 'ts/pages/account/account_apply_modal'; import { AccountDetail } from 'ts/pages/account/account_detail'; @@ -150,6 +150,8 @@ export const Account: React.FC = () => { const [votingPowerMap, setVotingPowerMap] = React.useState({}); const [hasVotingPower, setHasVotingPower] = React.useState(false); const [shouldOpenStakeDecisionModal, setOpenStakeDecisionModal] = React.useState(false); + + const [stakeRelabanceOpen, setStakeRebalanceOpen] = React.useState(false); // keeping in case we want to make use of by-pool estimated rewards // const [expectedCurrentEpochPoolRewards, setExpectedCurrentEpochPoolRewards] = React.useState(undefined); const [expectedCurrentEpochRewards, setExpectedCurrentEpochRewards] = React.useState(new BigNumber(0)); @@ -164,6 +166,7 @@ export const Account: React.FC = () => { withdrawStake, withdrawRewards, moveStake, + batchMoveStake, currentEpochRewards, error: useStakeError, } = useStake(networkId, providerState); @@ -457,8 +460,9 @@ export const Account: React.FC = () => { return { pool, zrxStaked: delegatorPoolStats.zrxStaked }; }); - console.log(delegatorData); - + const rebalanceStake = (rebalanceStakeData: MoveStakeData[]) => { + batchMoveStake(rebalanceStakeData, () => {}); + }; return ( @@ -603,7 +607,7 @@ export const Account: React.FC = () => { {/* TODO add loading animations or display partially loaded data */} {hasDataLoaded() && ( - + Your Staking Pools @@ -696,6 +700,21 @@ export const Account: React.FC = () => { ); }) )} +
+ +
)} {hasDataLoaded() && hasVotingPower && ( @@ -859,7 +878,16 @@ export const Account: React.FC = () => { - {}} poolData={poolData} stakingPools={stakingPools || []} /> + {stakeRelabanceOpen && ( + { + setStakeRebalanceOpen(false); + }} + poolData={poolData} + stakingPools={stakingPools || []} + rebalanceStake={rebalanceStake} + /> + )}
); }; From 8f20036c5f7d5f336510033f0ba6fee237457c2c Mon Sep 17 00:00:00 2001 From: Ben Lyaunzon Date: Mon, 5 Jul 2021 19:02:50 -0700 Subject: [PATCH 04/29] lintfix --- ts/components/slider/percentage_slider.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ts/components/slider/percentage_slider.tsx b/ts/components/slider/percentage_slider.tsx index ddb711860..bd4cd9f6d 100644 --- a/ts/components/slider/percentage_slider.tsx +++ b/ts/components/slider/percentage_slider.tsx @@ -1,3 +1,5 @@ +/* tslint:disable */ + import * as React from 'react'; const colors = require('nice-color-palettes'); @@ -141,12 +143,15 @@ export const PercentageSlider: React.FC = ({ pools, tags, setWidths(_widths); }; - + // tslint:disable-next-line window.addEventListener('pointermove', resize); + // tslint:disable-next-line window.addEventListener('touchmove', resize); const removeEventListener = () => { + // tslint:disable-next-line window.removeEventListener('pointermove', resize); + // tslint:disable-next-line window.removeEventListener('touchmove', resize); }; From 1789bf8259dded6ed3e73ea58e23262020d2b617 Mon Sep 17 00:00:00 2001 From: Ben Lyaunzon Date: Mon, 5 Jul 2021 19:14:09 -0700 Subject: [PATCH 05/29] tsc fix --- ts/components/slider/percentage_slider.tsx | 40 +++++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/ts/components/slider/percentage_slider.tsx b/ts/components/slider/percentage_slider.tsx index bd4cd9f6d..4a17e8561 100644 --- a/ts/components/slider/percentage_slider.tsx +++ b/ts/components/slider/percentage_slider.tsx @@ -114,9 +114,39 @@ export const PercentageSlider: React.FC = ({ pools, tags, const startDragX = e.pageX; const sliderWidth = TagSliderRef.current.offsetWidth; - const resize = (e: MouseEvent & TouchEvent & PointerEvent) => { - e.preventDefault(); - const endDragX = e.touches ? e.touches[0].pageX : e.pageX; + const resize = (ev: PointerEvent) => { + ev.preventDefault(); + const eTouch = (ev as unknown) as TouchEvent; + const endDragX = eTouch.touches ? eTouch.touches[0].pageX : ev.pageX; + const distanceMoved = endDragX - startDragX; + const maxPercent = widths[index] + widths[index + 1]; + + const percentageMoved = nearestN(1, getPercentage(sliderWidth, distanceMoved)); + // const percentageMoved = getPercentage(sliderWidth, distanceMoved); + + const _widths = widths.slice(); + + const prevPercentage = _widths[index]; + + const newPercentage = prevPercentage + percentageMoved; + const currentSectionWidth = limitNumberWithinRange(newPercentage, 0, maxPercent); + _widths[index] = currentSectionWidth; + + const nextSectionIndex = index + 1; + + const nextSectionNewPercentage = _widths[nextSectionIndex] - percentageMoved; + const nextSectionWidth = limitNumberWithinRange( + nextSectionNewPercentage, + 0, + maxPercent, + ); + _widths[nextSectionIndex] = nextSectionWidth; + + setWidths(_widths); + }; + const resizeTouch = (ev: TouchEvent) => { + ev.preventDefault(); + const endDragX = ev.touches[0].pageX; const distanceMoved = endDragX - startDragX; const maxPercent = widths[index] + widths[index + 1]; @@ -146,13 +176,13 @@ export const PercentageSlider: React.FC = ({ pools, tags, // tslint:disable-next-line window.addEventListener('pointermove', resize); // tslint:disable-next-line - window.addEventListener('touchmove', resize); + window.addEventListener('touchmove', resizeTouch); const removeEventListener = () => { // tslint:disable-next-line window.removeEventListener('pointermove', resize); // tslint:disable-next-line - window.removeEventListener('touchmove', resize); + window.removeEventListener('touchmove', resizeTouch); }; const handleEventUp = (e: Event) => { From da438104b3f0b4cfb23a9877f05775df3b7cfafe Mon Sep 17 00:00:00 2001 From: Ben Lyaunzon Date: Thu, 8 Jul 2021 00:59:23 -0700 Subject: [PATCH 06/29] lintfixed --- ts/components/staking/add_pool_dialog.tsx | 3 +-- ts/components/staking/stake_rebalance.tsx | 18 +----------------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/ts/components/staking/add_pool_dialog.tsx b/ts/components/staking/add_pool_dialog.tsx index f75493814..4dd2f29a2 100644 --- a/ts/components/staking/add_pool_dialog.tsx +++ b/ts/components/staking/add_pool_dialog.tsx @@ -1,4 +1,3 @@ -import { BigNumber } from '@0x/utils'; import { DialogContent, DialogOverlay } from '@reach/dialog'; import '@reach/dialog/styles.css'; import React, { FC, useMemo, useState } from 'react'; @@ -7,8 +6,8 @@ import styled from 'styled-components'; import { Button } from 'ts/components/button'; import { Icon } from 'ts/components/icon'; import { Input } from 'ts/components/modals/input'; -import { Heading, Paragraph } from 'ts/components/text'; import { Thumbnail } from 'ts/components/staking/thumbnail.tsx'; +import { Heading, Paragraph } from 'ts/components/text'; import { useSearch } from 'ts/hooks/use_search'; import { zIndex } from 'ts/style/z_index'; diff --git a/ts/components/staking/stake_rebalance.tsx b/ts/components/staking/stake_rebalance.tsx index 564c55479..3f405bf45 100644 --- a/ts/components/staking/stake_rebalance.tsx +++ b/ts/components/staking/stake_rebalance.tsx @@ -12,18 +12,15 @@ import { stakingUtils } from 'ts/utils/staking_utils'; import { Button } from 'ts/components/button'; import { Icon } from 'ts/components/icon'; import { AddPoolDialog } from 'ts/components/staking/add_pool_dialog'; -import { Loading } from 'ts/components/portal/loading'; import { ZRXInput } from 'ts/components/staking/staking_calculator'; import { PercentageSlider } from 'ts/components/slider/percentage_slider'; import { Heading } from 'ts/components/text'; -import { Select, SelectItemConfig } from 'ts/components/ui/select'; import { useAPIClient } from 'ts/hooks/use_api_client'; import { colors } from 'ts/style/colors'; -import { PoolEpochDelegatorStats, PoolWithStats } from 'ts/types'; +import { PoolWithStats } from 'ts/types'; -const paletteColors = require('nice-color-palettes'); const randPalette = ['#fe4365', '#fc9d9a', '#f9cdad', '#c8c8a9', '#83af9b']; const ButtonClose = styled(Button)` @@ -77,14 +74,6 @@ const HeadingWrapper = styled.div` font-weight: 400; `; -const RewardRow = styled.div` - display: flex; - justify-content: space-between; - font-size: 16px; - padding-bottom: 0.75rem; - margin-bottom: 0.85rem; -`; - const ColorBox = styled.div` width: 20px; height: 20px; @@ -137,9 +126,6 @@ interface StakeRebalanceProps { rebalanceStake: (rebalanceStakeData: MoveStakeData[]) => void; } export const StakeRebalance: React.FC = ({ onClose, poolData, stakingPools, rebalanceStake }) => { - const networkId = useSelector((state: State) => state.networkId); - const apiClient = useAPIClient(networkId); - const originalSumOfZrx = poolData .map((item) => item.zrxStaked) .reduce((accumulator, currentValue) => accumulator + currentValue); @@ -328,8 +314,6 @@ export const StakeRebalance: React.FC = ({ onClose, poolDat } }); - console.log(additions, reductions); - if (additions.length > 0) { const data = _.flatMap(additions, (item) => { return generateMoveStakeData(item, reductions); From 29ee85ca65859b6fb1dcd1fb155d216f4580c44c Mon Sep 17 00:00:00 2001 From: Ben Lyaunzon Date: Mon, 12 Jul 2021 00:58:31 -0700 Subject: [PATCH 07/29] fixing mentioned issues --- ts/components/staking/stake_rebalance.tsx | 15 +++++++++------ ts/pages/account/dashboard.tsx | 16 ++++++++++------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/ts/components/staking/stake_rebalance.tsx b/ts/components/staking/stake_rebalance.tsx index 3f405bf45..8a1229a55 100644 --- a/ts/components/staking/stake_rebalance.tsx +++ b/ts/components/staking/stake_rebalance.tsx @@ -273,6 +273,7 @@ export const StakeRebalance: React.FC = ({ onClose, poolDat toPoolId: poolId, zrxAmount: parseFloat((diff - accumulatedAmount).toFixed(2)), }); + continue; } accumulatedAmount += availAmt; moveStakeData.push({ @@ -372,12 +373,14 @@ export const StakeRebalance: React.FC = ({ onClose, poolDat ); })}
- + {currentPoolData.length > 1 && ( + + )} {currentPoolData.length < 4 && ( = () => { } const nextEpochStart = nextEpochStats && new Date(nextEpochStats.epochStart.timestamp); - const poolData = delegatorData.forCurrentEpoch.poolData.map((delegatorPoolStats: PoolEpochDelegatorStats) => { - const poolId = delegatorPoolStats.poolId; - const pool = poolWithStatsMap[poolId]; - return { pool, zrxStaked: delegatorPoolStats.zrxStaked }; - }); + const poolData = delegatorData.forCurrentEpoch.poolData + .map((delegatorPoolStats: PoolEpochDelegatorStats) => { + const poolId = delegatorPoolStats.poolId; + const pool = poolWithStatsMap[poolId]; + return { pool, zrxStaked: delegatorPoolStats.zrxStaked }; + }) + .filter((item) => item.zrxStaked > 0); const rebalanceStake = (rebalanceStakeData: MoveStakeData[]) => { - batchMoveStake(rebalanceStakeData, () => {}); + batchMoveStake(rebalanceStakeData, () => { + setStakeRebalanceOpen(false); + }); }; return ( From 200e4876d49bf8c756758993c2adf3ef850a1f8b Mon Sep 17 00:00:00 2001 From: Ben Lyaunzon Date: Tue, 13 Jul 2021 16:52:38 -0700 Subject: [PATCH 08/29] lintfixes --- ts/components/staking/stake_rebalance.tsx | 3 -- ts/utils/algolia_meta.json | 41 +++++++++++++++++++---- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/ts/components/staking/stake_rebalance.tsx b/ts/components/staking/stake_rebalance.tsx index 8a1229a55..b23a3234d 100644 --- a/ts/components/staking/stake_rebalance.tsx +++ b/ts/components/staking/stake_rebalance.tsx @@ -2,11 +2,9 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { useSelector } from 'react-redux'; import styled from 'styled-components'; import { MoveStakeData } from 'ts/hooks/use_stake'; -import { State } from 'ts/redux/reducer'; import { stakingUtils } from 'ts/utils/staking_utils'; import { Button } from 'ts/components/button'; @@ -16,7 +14,6 @@ import { ZRXInput } from 'ts/components/staking/staking_calculator'; import { PercentageSlider } from 'ts/components/slider/percentage_slider'; import { Heading } from 'ts/components/text'; -import { useAPIClient } from 'ts/hooks/use_api_client'; import { colors } from 'ts/style/colors'; import { PoolWithStats } from 'ts/types'; diff --git a/ts/utils/algolia_meta.json b/ts/utils/algolia_meta.json index f24fadebd..452242d14 100644 --- a/ts/utils/algolia_meta.json +++ b/ts/utils/algolia_meta.json @@ -158,7 +158,12 @@ "title": "0x Coordinator Specification (v2)", "description": "A comprehensive technical spec of the first coordinator model developer by the 0x core team (soft-cancel variant)", "tags": ["Coordinator", "Relayer", "Trader", "Protocol Developer"], - "topics": ["Coordinator", "Relayer", "Trader", "Protocol Developer"], + "topics": [ + "Coordinator", + "Relayer", + "Trader", + "Protocol Developer" + ], "difficulty": "Advanced", "path": "guides/v2-coordinator-specification.mdx" }, @@ -206,16 +211,40 @@ "v3-staking-specification": { "title": "0x Staking Specification (v3)", "description": "The technical spec for the Staking in 0x protocol v3. Allowing Market Makers to receive rebates when orders are filled.", - "tags": ["Specs", "Relayer", "Trader", "Market Maker", "Protocol Developer"], - "topics": ["Specs", "Relayer", "Trader", "Market Maker", "Protocol Developer"], + "tags": [ + "Specs", + "Relayer", + "Trader", + "Market Maker", + "Protocol Developer" + ], + "topics": [ + "Specs", + "Relayer", + "Trader", + "Market Maker", + "Protocol Developer" + ], "difficulty": "Advanced", "path": "guides/v3-staking-specification.mdx" }, "v3-coordinator-specification": { "title": "0x Coordinator Specification (v3)", "description": "A comprehensive technical spec of the first coordinator model developer by the 0x core team (soft-cancel variant)", - "tags": ["Coordinator", "Relayer", "Trader", "Market Maker", "Protocol Developer"], - "topics": ["Coordinator", "Relayer", "Trader", "Market Maker", "Protocol Developer"], + "tags": [ + "Coordinator", + "Relayer", + "Trader", + "Market Maker", + "Protocol Developer" + ], + "topics": [ + "Coordinator", + "Relayer", + "Trader", + "Market Maker", + "Protocol Developer" + ], "difficulty": "Advanced", "path": "guides/v3-coordinator-specification.mdx" }, @@ -603,4 +632,4 @@ "externalUrl": "https://github.com/0xProject/0x-coordinator-server" } } -} +} \ No newline at end of file From a25a3d2483f1544e1e6377700df05df62ac955ff Mon Sep 17 00:00:00 2001 From: Ben Lyaunzon Date: Sun, 25 Jul 2021 17:18:36 -0700 Subject: [PATCH 09/29] linting --- ts/components/staking/stake_rebalance.tsx | 132 +++++++++++----------- ts/pages/account/dashboard.tsx | 12 +- 2 files changed, 69 insertions(+), 75 deletions(-) diff --git a/ts/components/staking/stake_rebalance.tsx b/ts/components/staking/stake_rebalance.tsx index b23a3234d..c563174da 100644 --- a/ts/components/staking/stake_rebalance.tsx +++ b/ts/components/staking/stake_rebalance.tsx @@ -9,9 +9,9 @@ import { stakingUtils } from 'ts/utils/staking_utils'; import { Button } from 'ts/components/button'; import { Icon } from 'ts/components/icon'; +import { PercentageSlider } from 'ts/components/slider/percentage_slider'; import { AddPoolDialog } from 'ts/components/staking/add_pool_dialog'; import { ZRXInput } from 'ts/components/staking/staking_calculator'; -import { PercentageSlider } from 'ts/components/slider/percentage_slider'; import { Heading } from 'ts/components/text'; @@ -165,8 +165,8 @@ export const StakeRebalance: React.FC = ({ onClose, poolDat }; const addPool = (poolId: string) => { - const pool = stakingPools.find((pool) => { - return pool.poolId === poolId; + const pool = stakingPools.find((stakingPool) => { + return stakingPool.poolId === poolId; }); const updatedCurrentPoolData = [ ...currentPoolData, @@ -261,8 +261,7 @@ export const StakeRebalance: React.FC = ({ onClose, poolDat const { poolId, diff } = poolDiff; let accumulatedAmount = 0; const moveStakeData = []; - for (let index = 0; index < reductions.length; index++) { - const element = reductions[index]; + for (const element of reductions) { const availAmt = Math.abs(element.diff); if (availAmt >= diff - accumulatedAmount) { moveStakeData.push({ @@ -290,12 +289,7 @@ export const StakeRebalance: React.FC = ({ onClose, poolDat const foundPool = poolData.find((foundItem) => { return item.pool.poolId === foundItem.pool.poolId; }); - let diff = 0; - if (foundPool) { - diff = item.zrxStaked - foundPool.zrxStaked; - } else { - diff = item.zrxStaked; - } + const diff = foundPool ? item.zrxStaked - foundPool.zrxStaked : item.zrxStaked; if (diff !== 0) { if (diff > 0) { @@ -326,24 +320,24 @@ export const StakeRebalance: React.FC = ({ onClose, poolDat - {true && ( - <> - - - Change your Stake - - - - {currentPoolData.map((data, index) => { - const poolTag = poolTags.find((item) => { - return item.name === data.pool.poolId; - }); - - const isStartingPool = poolData.find((item) => { - return item.pool.poolId === data.pool.poolId; - }); - - return ( + <> + + + Change your Stake + + + + {currentPoolData.map((data, index) => { + const poolTag = poolTags.find((item) => { + return item.name === data.pool.poolId; + }); + + const isStartingPool = poolData.find((item) => { + return item.pool.poolId === data.pool.poolId; + }); + + return ( +
@@ -367,47 +361,47 @@ export const StakeRebalance: React.FC = ({ onClose, poolDat }} /> - ); - })} - - {currentPoolData.length > 1 && ( - - )} - - {currentPoolData.length < 4 && ( - { - setIsAddPoolDialogOpen(true); - }} - > - + Add Pool - - )} - - - { - setIsAddPoolDialogOpen(false); - }} - onAddPool={addPool} +
+ ); + })} +
+ {currentPoolData.length > 1 && ( + - - )} + )} + + {currentPoolData.length < 4 && ( + { + setIsAddPoolDialogOpen(true); + }} + > + + Add Pool + + )} + + + { + setIsAddPoolDialogOpen(false); + }} + onAddPool={addPool} + /> +
); diff --git a/ts/pages/account/dashboard.tsx b/ts/pages/account/dashboard.tsx index 2d40fdfcd..300801eaf 100644 --- a/ts/pages/account/dashboard.tsx +++ b/ts/pages/account/dashboard.tsx @@ -19,7 +19,7 @@ import { Heading, Paragraph } from 'ts/components/text'; import { InfoTooltip } from 'ts/components/ui/info_tooltip'; import { StatFigure } from 'ts/components/ui/stat_figure'; import { useAPIClient } from 'ts/hooks/use_api_client'; -import { useStake, MoveStakeData } from 'ts/hooks/use_stake'; +import { MoveStakeData, useStake } from 'ts/hooks/use_stake'; import { AccountActivitySummary } from 'ts/pages/account/account_activity_summary'; import { AccountApplyModal } from 'ts/pages/account/account_apply_modal'; import { AccountDetail } from 'ts/pages/account/account_detail'; @@ -151,7 +151,7 @@ export const Account: React.FC = () => { const [hasVotingPower, setHasVotingPower] = React.useState(false); const [shouldOpenStakeDecisionModal, setOpenStakeDecisionModal] = React.useState(false); - const [stakeRelabanceOpen, setStakeRebalanceOpen] = React.useState(false); + const [isRebalanceOpen, setIsRebalanceOpen] = React.useState(false); // keeping in case we want to make use of by-pool estimated rewards // const [expectedCurrentEpochPoolRewards, setExpectedCurrentEpochPoolRewards] = React.useState(undefined); const [expectedCurrentEpochRewards, setExpectedCurrentEpochRewards] = React.useState(new BigNumber(0)); @@ -464,7 +464,7 @@ export const Account: React.FC = () => { const rebalanceStake = (rebalanceStakeData: MoveStakeData[]) => { batchMoveStake(rebalanceStakeData, () => { - setStakeRebalanceOpen(false); + setIsRebalanceOpen(false); }); }; return ( @@ -713,7 +713,7 @@ export const Account: React.FC = () => { + + + // tslint:disable-next-line: jsx-curly-spacing + } + /> + {/* Govern 0x Protocol @@ -304,8 +335,8 @@ export const VoteIndex: React.FC = () => { - - + */} + {/* */} {isLoading ? ( diff --git a/ts/utils/backend_client.ts b/ts/utils/backend_client.ts index e2dbc5155..fa944e848 100644 --- a/ts/utils/backend_client.ts +++ b/ts/utils/backend_client.ts @@ -137,6 +137,32 @@ export const backendClient = { }, async getSnapshotProposalAsync() {}, + async getTreasuryTokenPrices() { + const treasuryTokenCGIds = ['0x', 'matic-network']; + const cgSimplePriceBaseUri = 'https://api.coingecko.com/api/v3/simple/price'; + const res = fetchUtils.requestAsync( + cgSimplePriceBaseUri, + `?ids=${treasuryTokenCGIds.join(',')}&vs_currencies=usd`, + ); + return res; + }, + + async getTreasuryTokenTransfers() { + const ZRX_TOKEN = '0xe41d2489571d322189246dafa5ebde1f4699f498'; + const MATIC_TOKEN = '0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0'; + const reqBaseUri = + 'https://api.covalenthq.com/v1/1/address/0x0bB1810061C2f5b2088054eE184E6C79e1591101/transfers_v2/'; + const zrxTransfers = fetchUtils.requestAsync( + reqBaseUri, + `?contract-address=${ZRX_TOKEN}&key=ckey_02c853f8bd48448190555163e59`, + ); + const maticTransfers = fetchUtils.requestAsync( + reqBaseUri, + `?contract-address=${MATIC_TOKEN}&key=ckey_02c853f8bd48448190555163e59`, + ); + return await Promise.all([zrxTransfers, maticTransfers]); + }, + async getGasInfoAsync(speed?: string): Promise { // Median gas prices across 0x api gas oracles // Defaulting to average/standard gas. Using eth gas station for time estimates From 9e2172739bb541306d97ae095d2b94f355bf7b38 Mon Sep 17 00:00:00 2001 From: Ben Lyaunzon Date: Tue, 31 Aug 2021 00:35:49 -0700 Subject: [PATCH 19/29] minor gov index page fixes --- ts/components/governance/hero.tsx | 309 +++++++++++++++++++++++++++++ ts/pages/governance/vote_index.tsx | 19 +- 2 files changed, 323 insertions(+), 5 deletions(-) create mode 100644 ts/components/governance/hero.tsx diff --git a/ts/components/governance/hero.tsx b/ts/components/governance/hero.tsx new file mode 100644 index 000000000..515fc8d0a --- /dev/null +++ b/ts/components/governance/hero.tsx @@ -0,0 +1,309 @@ +import * as React from 'react'; +import styled from 'styled-components'; +import { Progressbar } from 'ts/components/progressbar'; +import { stakingUtils } from 'ts/utils/staking_utils'; +import { configs, GOVERNANCE_THEGRAPH_ENDPOINT, GOVERNOR_CONTRACT_ADDRESS } from 'ts/utils/configs'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import { formatNumber } from 'ts/utils/format_number'; + +import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses'; +import { ERC20TokenContract } from '@0x/contract-wrappers'; + +import { BigNumber } from '@0x/utils'; +import { useSelector } from 'react-redux'; +import { utils } from 'ts/utils/utils'; + +import { State } from 'ts/redux/reducer'; +import { backendClient } from 'ts/utils/backend_client'; + +import { differenceInSeconds } from 'date-fns'; + +import { formatEther, formatZrx } from 'ts/utils/format_number'; + +import { colors } from 'ts/style/colors'; +import { ZrxTreasuryContract } from '@0x/contracts-treasury'; +import { ZeroExProvider } from '@0x/asset-buyer'; + +interface GovernanceHeroProps { + title: string | React.ReactNode; + titleMobile: string | React.ReactNode; + description: string | React.ReactNode; + figure: React.ReactNode; + actions: React.ReactNode; + provider?: ZeroExProvider; + videoId?: string; + videoChannel?: string; + videoRatio?: string; + youtubeOptions?: any; + metrics?: { + zrxStaked: number; + currentEpochRewards: BigNumber; + nextEpochStartDate: Date; + }; +} + +interface WrapperProps {} + +interface InnerProps {} + +interface RowProps {} + +const ProgressbarText = styled.span` + display: block; + font-size: 15px; + color: #5c5c5c; + line-height: 1.2; + margin-top: 8px; +`; + +const Wrapper = styled.div` + width: 100%; + text-align: center; + max-width: 1450px; + margin: 0 auto; + @media (min-width: 768px) { + padding: 30px; + text-align: left; + } +`; + +const Inner = styled.div` + background-color: #f3f6f4; + background-image: url(/images/stakingGraphic.svg); + background-repeat: no-repeat; + background-position-x: right; + background-position-y: center; + @media (min-width: 768px) { + padding: 30px; + } +`; + +const Row = styled.div` + max-width: 1152px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; + flex-direction: column; + @media (min-width: 768px) { + flex-direction: row; + & > * { + } + } +`; + +const Column = styled.div` + padding: 30px; + @media (min-width: 768px) { + padding: 60px 28px; + &:first-child { + padding-left: 0; + } + &:last-child { + padding-right: 0; + } + } +`; + +const Title = styled.h1` + font-size: 46px; + line-height: 1.2; + font-weight: 300; + margin-bottom: 20px; + display: none; + @media (min-width: 768px) { + font-size: 50px; + display: block; + } +`; + +const TitleMobile = styled(Title)` + display: block; + @media (min-width: 768px) { + display: none; + } +`; + +const Description = styled.h2` + font-size: 18px; + line-height: 1.45; + font-weight: 300; + margin-bottom: 30px; + color: ${colors.textDarkSecondary}; +`; + +const Actions = styled.div` + & > * { + margin-right: 13px; + margin-bottom: 10px; + } +`; + +const MetricsWrapper = styled.div` + display: flex; + flex-direction: column; +`; + +const FiguresList = styled.ol` + display: flex; + flex-direction: column; + flex-wrap: wrap; + padding-top: 15px; + width: 250px; +`; + +const Figure = styled.li` + display: flex; + flex-direction: column; + justify-content: space-between; + text-align: left; + background-color: ${colors.white}; + padding: 10px; + margin-bottom: 15px; + @media (min-width: 480px) { + padding: 20px; + } +`; + +const FigureHeader = styled.header` + display: flex; + justify-content: space-between; + align-items: baseline; +`; + +const FigureTitle = styled.span` + display: block; + font-size: 16px; + line-height: 1.35; + color: #999999; + margin-bottom: 5px; +`; + +const FigureNumber = styled.span` + display: block; + font-feature-settings: 'tnum' on, 'lnum' on; + font-size: 20px; + line-height: 1.35; + @media (min-width: 768px) { + font-size: 24px; + } + @media (min-width: 991px) { + font-size: 28px; + } +`; + +const FiguresListHeader = styled.h2``; + +type TreasuryTokenPricesUsd = { + zrx: number; + matic: number; +}; +export const GovernanceHero: React.FC = (props) => { + const { title, titleMobile, description, actions, provider, metrics } = props; + const providerState = useSelector((state: State) => state.providerState); + + const [totalTreasuryAmountUSD, setTotalTreasuryAmountUSD] = React.useState('-'); + const [totalTreasuryDistributedUSD, setTotalTreasuryDistributedUSD] = React.useState('-'); + + const parseTotalDistributed = (transferData: any) => { + let totalDistributed = 0; + if (transferData) { + transferData.forEach((tf: any) => { + if (tf.data.items) { + tf.data.items.forEach((item: any) => { + item.transfers.forEach((transfer: any) => { + if (transfer.transfer_type === 'OUT') { + totalDistributed += transfer.delta_quote; + } + }); + }); + } + }); + } + + return totalDistributed; + }; + + React.useEffect(() => { + const zrxTokenContract = new ERC20TokenContract( + '0xe41d2489571d322189246dafa5ebde1f4699f498', + providerState.provider, + ); + const maticTokenContract = new ERC20TokenContract( + '0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0', + providerState.provider, + ); + + (async () => { + const [zrxBalance, maticBalance] = await Promise.all([ + zrxTokenContract.balanceOf(GOVERNOR_CONTRACT_ADDRESS.ZRX).callAsync(), + maticTokenContract.balanceOf(GOVERNOR_CONTRACT_ADDRESS.ZRX).callAsync(), + ]); + const res = await backendClient.getTreasuryTokenPrices(); + const zrxAmount = Web3Wrapper.toUnitAmount(zrxBalance, 18); + const maticAmount = Web3Wrapper.toUnitAmount(maticBalance, 18); + const zrxUSD = zrxAmount.multipliedBy(res['0x'].usd); + const maticUSD = maticAmount.multipliedBy(res['matic-network'].usd); + + const treasuryTokenTransferData = await backendClient.getTreasuryTokenTransfers(); + const totalDistributed = parseTotalDistributed(treasuryTokenTransferData); + console.log(totalDistributed); + setTotalTreasuryDistributedUSD( + '$' + + formatNumber(totalDistributed, { + decimals: 6, + decimalsRounded: 6, + bigUnitPostfix: true, + }).formatted, + ); + setTotalTreasuryAmountUSD( + '$' + + formatNumber(zrxUSD.plus(maticUSD).toString(), { + decimals: 6, + decimalsRounded: 6, + bigUnitPostfix: true, + }).formatted, + ); + })(); + }, [providerState]); + + return ( + + + + + {title} + {titleMobile} + {description} + {actions} + + + + Treasury Stats + +
+ + Available Treasury + + {totalTreasuryAmountUSD} +
+
+ + Total Distributed + + {totalTreasuryDistributedUSD} +
+ {/* Treasury Details */} +
+
+
+
+
+
+ ); +}; + +GovernanceHero.defaultProps = { + videoChannel: 'youtube', + videoRatio: '21:9', +}; diff --git a/ts/pages/governance/vote_index.tsx b/ts/pages/governance/vote_index.tsx index b08c0c8bc..0e2c1d438 100644 --- a/ts/pages/governance/vote_index.tsx +++ b/ts/pages/governance/vote_index.tsx @@ -293,17 +293,26 @@ export const VoteIndex: React.FC = () => { with your ZRX } - titleMobile="Earn liquidity rewards with ZRX" - description={
Vote to change the infastructure of internet finance
} + titleMobile="Make an impact with your ZRX" + description={
Govern the exchange infrastructure of the Internet
} figure={<>} videoId="qP_oZAjRkTs" actions={ <> -