From fdd3fbd7747a033d371a5e920cdca4dc8c6b4fe2 Mon Sep 17 00:00:00 2001 From: Bill Date: Wed, 4 Oct 2023 13:11:35 -0400 Subject: [PATCH] Reward page (#52) * Implement Rewards Page * more rewards page panels * add keplr dialog * Finish up rewards page * bump localization * add to nav * address comments * Comment out migration work * rename * address comments * add TODO --- .env.example | 2 + package.json | 2 +- pnpm-lock.yaml | 8 +- src/components/Link.tsx | 6 +- src/components/Panel.tsx | 4 +- src/constants/dialogs.ts | 1 + src/hooks/useAccountBalance.ts | 9 +- src/layout/DialogManager.tsx | 2 + src/layout/Header/HeaderDesktop.tsx | 5 + src/lib/abacus/stateNotification.ts | 8 + src/pages/rewards/DYDXBalancePanel.tsx | 18 +- src/pages/rewards/RewardsPage.tsx | 328 ++++++++++++++++++- src/state/account.ts | 5 + src/state/accountSelectors.ts | 5 + src/views/dialogs/ExternalNavKeplrDialog.tsx | 107 ++++++ 15 files changed, 484 insertions(+), 26 deletions(-) create mode 100644 src/views/dialogs/ExternalNavKeplrDialog.tsx diff --git a/.env.example b/.env.example index c47d6b7..84ce9c6 100644 --- a/.env.example +++ b/.env.example @@ -6,3 +6,5 @@ VITE_PK_ENCRYPTION_KEY= VITE_WALLETCONNECT1_BRIDGE= VITE_WALLETCONNECT2_PROJECT_ID= + +VITE_V3_TOKEN_ADDRESS= \ No newline at end of file diff --git a/package.json b/package.json index e419f53..cb5c058 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@cosmjs/tendermint-rpc": "^0.31.0", "@dydxprotocol/v4-abacus": "^0.6.3", "@dydxprotocol/v4-client-js": "^0.36.1", - "@dydxprotocol/v4-localization": "^0.1.12", + "@dydxprotocol/v4-localization": "^0.1.23", "@ethersproject/providers": "^5.7.2", "@js-joda/core": "^5.5.3", "@radix-ui/react-collapsible": "^1.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 280c5ed..5c03d6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,8 +33,8 @@ dependencies: specifier: ^0.36.1 version: 0.36.1 '@dydxprotocol/v4-localization': - specifier: ^0.1.12 - version: 0.1.12 + specifier: ^0.1.23 + version: 0.1.23 '@ethersproject/providers': specifier: ^5.7.2 version: 5.7.2 @@ -1010,8 +1010,8 @@ packages: - utf-8-validate dev: false - /@dydxprotocol/v4-localization@0.1.12: - resolution: {integrity: sha512-EiP/8+6Nk9QBa6YSWsyzsZaXoVbz5o/KgUlMp1Cew6VWkXL6DkQY0oMIEEy21pv0zuu/woxhlbyXygpUIcpdMg==} + /@dydxprotocol/v4-localization@0.1.23: + resolution: {integrity: sha512-TaEey7dINwxELlEyA8XsQ4GQLfJ7e1b434bafpnlG9ccW1sIW7TYTsfaSkck2egR4R736hA7k5WvVy0aI/0TFw==} dev: false /@dydxprotocol/v4-proto@0.2.1: diff --git a/src/components/Link.tsx b/src/components/Link.tsx index ee5bef0..bbf89a0 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -12,7 +12,7 @@ type ElementProps = { }; children: React.ReactNode; href?: string; - onClick?: () => void; + onClick?: (e: MouseEvent) => void; withIcon?: boolean; }; @@ -37,12 +37,12 @@ export const Link = forwardRef( ref={ref} className={className} href={href} - onClick={() => { + onClick={(e: MouseEvent) => { if (analyticsConfig) { console.log(analyticsConfig); } - onClick?.(); + onClick?.(e); }} rel="noopener noreferrer" target="_blank" diff --git a/src/components/Panel.tsx b/src/components/Panel.tsx index 6873f96..cd33955 100644 --- a/src/components/Panel.tsx +++ b/src/components/Panel.tsx @@ -11,6 +11,7 @@ type PanelProps = { children?: React.ReactNode; href?: string; onHeaderClick?: () => void; + onClick?: () => void; }; type PanelStyleProps = { @@ -24,10 +25,11 @@ export const Panel = ({ children, href, onHeaderClick, + onClick, hasSeparator, className, }: PanelProps & PanelStyleProps) => ( - + {href ? ( {slotHeader ? ( diff --git a/src/constants/dialogs.ts b/src/constants/dialogs.ts index ed20d3f..c14c0fb 100644 --- a/src/constants/dialogs.ts +++ b/src/constants/dialogs.ts @@ -5,6 +5,7 @@ export enum DialogTypes { ExchangeOffline = 'ExchangeOffline', FillDetails = 'FillDetails', Help = 'Help', + ExternalNavKeplr = 'ExternalNavKeplr', MnemonicExport = 'MnemonicExport', MobileSignIn = 'MobileSignIn', Onboarding = 'Onboarding', diff --git a/src/hooks/useAccountBalance.ts b/src/hooks/useAccountBalance.ts index 88a6858..64f8334 100644 --- a/src/hooks/useAccountBalance.ts +++ b/src/hooks/useAccountBalance.ts @@ -14,7 +14,7 @@ import { EvmAddress } from '@/constants/wallets'; import { convertBech32Address } from '@/lib/addressUtils'; import { MustBigNumber } from '@/lib/numbers'; -import { getBalances } from '@/state/accountSelectors'; +import { getBalances, getStakingBalances } from '@/state/accountSelectors'; import { getSelectedNetwork } from '@/state/appSelectors'; import { useAccounts } from './useAccounts'; @@ -53,6 +53,7 @@ export const useAccountBalance = ({ const selectedNetwork = useSelector(getSelectedNetwork); const balances = useSelector(getBalances, shallowEqual); const evmChainId = Number(ENVIRONMENT_CONFIG_MAP[selectedNetwork].ethereumChainId); + const stakingBalances = useSelector(getStakingBalances, shallowEqual); const evmQuery = useBalance({ enabled: Boolean(!isCosmosChain && addressOrDenom?.startsWith('0x')), @@ -100,9 +101,15 @@ export const useAccountBalance = ({ const usdcCoinBalance = balances?.[USDC_DENOM]; const usdcBalance = MustBigNumber(usdcCoinBalance?.amount).div(QUANTUM_MULTIPLIER).toNumber(); + const nativeStakingCoinBalanace = stakingBalances?.[DYDX_DENOM]; + const nativeStakingBalance = MustBigNumber(nativeStakingCoinBalanace?.amount) + .div(QUANTUM_MULTIPLIER) + .toNumber(); + return { balance, nativeTokenBalance, + nativeStakingBalance, usdcBalance, queryStatus: isCosmosChain ? cosmosQuery.status : evmQuery.status, isQueryFetching: isCosmosChain ? cosmosQuery.isFetching : evmQuery.fetchStatus === 'fetching', diff --git a/src/layout/DialogManager.tsx b/src/layout/DialogManager.tsx index e647bc2..4a7d0f0 100644 --- a/src/layout/DialogManager.tsx +++ b/src/layout/DialogManager.tsx @@ -11,6 +11,7 @@ import { DepositDialog } from '@/views/dialogs/DepositDialog'; import { DisconnectDialog } from '@/views/dialogs/DisconnectDialog'; import { ExchangeOfflineDialog } from '@/views/dialogs/ExchangeOfflineDialog'; import { HelpDialog } from '@/views/dialogs/HelpDialog'; +import { ExternalNavKeplrDialog } from '@/views/dialogs/ExternalNavKeplrDialog'; import { MnemonicExportDialog } from '@/views/dialogs/MnemonicExportDialog'; import { MobileSignInDialog } from '@/views/dialogs/MobileSignInDialog'; import { OnboardingDialog } from '@/views/dialogs/OnboardingDialog'; @@ -47,6 +48,7 @@ export const DialogManager = () => { [DialogTypes.ExchangeOffline]: , [DialogTypes.FillDetails]: , [DialogTypes.Help]: , + [DialogTypes.ExternalNavKeplr]: , [DialogTypes.MnemonicExport]: , [DialogTypes.MobileSignIn]: , [DialogTypes.Onboarding]: , diff --git a/src/layout/Header/HeaderDesktop.tsx b/src/layout/Header/HeaderDesktop.tsx index 24e256a..20ceb75 100644 --- a/src/layout/Header/HeaderDesktop.tsx +++ b/src/layout/Header/HeaderDesktop.tsx @@ -42,6 +42,11 @@ export const HeaderDesktop = () => { label: stringGetter({ key: STRING_KEYS.TRADE }), href: AppRoute.Trade, }, + { + value: 'Dv4TNT', + label: 'Dv4TNT', + href: AppRoute.Rewards, + }, { value: 'PORTFOLIO', label: stringGetter({ key: STRING_KEYS.PORTFOLIO }), diff --git a/src/lib/abacus/stateNotification.ts b/src/lib/abacus/stateNotification.ts index 573a9f2..6f9d92e 100644 --- a/src/lib/abacus/stateNotification.ts +++ b/src/lib/abacus/stateNotification.ts @@ -20,6 +20,7 @@ import type { RootStore } from '@/state/_store'; import { setBalances, + setStakingBalances, setFills, setFundingPayments, setHistoricalPnl, @@ -82,6 +83,13 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol { } dispatch(setBalances(balances)); } + if (updatedState.account?.stakingBalances) { + const stakingBalances: Record = {} + for (const { k, v } of updatedState.account.stakingBalances.toArray()) { + stakingBalances[k] = v; + } + dispatch(setStakingBalances(stakingBalances)); + } } if (changes.has(Changes.configs)) { diff --git a/src/pages/rewards/DYDXBalancePanel.tsx b/src/pages/rewards/DYDXBalancePanel.tsx index 5ebf66b..93bba51 100644 --- a/src/pages/rewards/DYDXBalancePanel.tsx +++ b/src/pages/rewards/DYDXBalancePanel.tsx @@ -29,10 +29,10 @@ export const DYDXBalancePanel = () => { const { walletType } = useAccounts(); const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual); - const { nativeTokenBalance } = useAccountBalance(); + const { nativeTokenBalance, nativeStakingBalance } = useAccountBalance(); return ( - @@ -97,7 +97,7 @@ export const DYDXBalancePanel = () => { ), - value: , + value: , }, ]} /> @@ -106,21 +106,17 @@ export const DYDXBalancePanel = () => { { key: 'totalBalance', label: 'Total balance', - value: , + value: , }, ]} /> - + ); }; const Styled: Record = {}; -Styled.TransfersCard = styled(Panel)` - width: 21.25rem; -`; - Styled.Header = styled.div` ${layoutMixins.spacedRow} padding: 1.25rem 1.5rem 0.5rem; @@ -174,13 +170,15 @@ Styled.IconContainer = styled.div` Styled.WalletAndStakedBalance = styled(Details)` --details-item-backgroundColor: var(--color-layer-6); + + grid-template-columns: 1fr 1fr; gap: 0.5rem; + > div { gap: 1rem; padding: 1rem; - width: 8.625rem; border-radius: 0.75em; background-color: var(--color-layer-5); diff --git a/src/pages/rewards/RewardsPage.tsx b/src/pages/rewards/RewardsPage.tsx index f760b98..ff8b0cd 100644 --- a/src/pages/rewards/RewardsPage.tsx +++ b/src/pages/rewards/RewardsPage.tsx @@ -1,17 +1,333 @@ import styled, { AnyStyledComponent } from 'styled-components'; +import { useDispatch, useSelector } from 'react-redux'; +import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; +import { STRING_KEYS } from '@/constants/localization'; +import { ButtonAction, ButtonSize, ButtonState, ButtonType } from '@/constants/buttons'; +import { DialogTypes } from '@/constants/dialogs'; + +import { useAccountBalance, useBreakpoints, useStringGetter } from '@/hooks'; + +import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; +import { Details } from '@/components/Details'; +import { Panel } from '@/components/Panel'; +import { IconName } from '@/components/Icon'; +import { Button } from '@/components/Button'; +import { IconButton } from '@/components/IconButton'; +import { Link } from '@/components/Link'; +import { Output, OutputType } from '@/components/Output'; +import { VerticalSeparator } from '@/components/Separator'; +import { WithReceipt } from '@/components/WithReceipt'; + +import { openDialog } from '@/state/dialogs'; +import { getSelectedNetwork } from '@/state/appSelectors'; + import { DYDXBalancePanel } from './DYDXBalancePanel'; -export const RewardsPage = () => ( - - - -); +// TODO: replace placeholder URL with real URLs when avaialble +const GOVERNANCE_HELP_URL = 'https://help.dydx.exchange/'; +const STAKING_HELP_URL = 'https://help.dydx.exchange/'; + +export const RewardsPage = () => { + const dispatch = useDispatch(); + const stringGetter = useStringGetter(); + const { isTablet, isNotTablet } = useBreakpoints(); + + const selectedNetwork = useSelector(getSelectedNetwork); + + // const chainId = Number(ENVIRONMENT_CONFIG_MAP[selectedNetwork].ethereumChainId); + + // const { balance } = useAccountBalance({ + // addressOrDenom: import.meta.env.VITE_V3_TOKEN_ADDRESS, + // assetSymbol: 'DYDX', + // chainId, + // isCosmosChain: false, + // }); + + // const tokenBalance = import.meta.env.VITE_V3_TOKEN_ADDRESS ? balance : 0; + + return ( + + {/* {isNotTablet ? ( + + +
+ + {stringGetter({ key: STRING_KEYS.MIGRATE })} + + + {stringGetter({ key: STRING_KEYS.MIGRATE_DESCRIPTION })} + + {stringGetter({ key: STRING_KEYS.LEARN_MORE })} → + + +
+ +
+
{stringGetter({ key: STRING_KEYS.AVAILABLE_TO_MIGRATE })}
+
+ +
+
+ +
+
+
+ ) : ( + +

{stringGetter({ key: STRING_KEYS.MIGRATE })}

+ + + {stringGetter({ + key: STRING_KEYS.FROM_TO, + params: { FROM: Ethereum, TO: dYdX Chain }, + })} + + + } + > + , + }, + ]} + /> + } + > + + + + {stringGetter({ key: STRING_KEYS.WANT_TO_LEARN })} + {stringGetter({ key: STRING_KEYS.CLICK_HERE })} + +
+ )} */} + + + {isTablet && ( + + + + )} + + {stringGetter({ key: STRING_KEYS.GOVERNANCE })}} + onClick={() => dispatch(openDialog({ type: DialogTypes.ExternalNavKeplr }))} + > + + + {stringGetter({ key: STRING_KEYS.GOVERNANCE_DESCRIPTION })} + e.stopPropagation()}> + {stringGetter({ key: STRING_KEYS.LEARN_MORE })} → + + + {/* TODO: vertically center based on Panel height */} + + + + + {stringGetter({ key: STRING_KEYS.STAKING })}} + onClick={() => dispatch(openDialog({ type: DialogTypes.ExternalNavKeplr }))} + > + + + {stringGetter({ key: STRING_KEYS.STAKING_DESCRIPTION })} + e.stopPropagation()}> + {stringGetter({ key: STRING_KEYS.LEARN_MORE })} → + + + {/* TODO: vertically center based on Panel height */} + + + + + {isNotTablet && ( + + + + )} + +
+ ); +}; const Styled: Record = {}; Styled.Page = styled.div` - ${layoutMixins.centered} + ${layoutMixins.contentContainerPage} + gap: 1.5rem; + + @media ${breakpoints.tablet} { + padding: 1rem; + } +`; + +Styled.Panel = styled(Panel)` + padding: 0 1.5rem 1rem; + + @media ${breakpoints.tablet} { + max-width: calc(100vw - 2rem); + } +`; + +Styled.Row = styled.div` + ${layoutMixins.spacedRow} + gap: 1rem; + + align-items: center; +`; + +Styled.Description = styled.div` + color: var(--color-text-0); + + a { + color: var(--color-text-1); + } +`; + +Styled.Migrate = styled.section` + max-width: min(100vw, var(--content-max-width)); + + padding: 1.5rem; + + background-color: var(--color-layer-3); + border-radius: 0.875rem; +`; + +Styled.TwoItemRow = styled(Styled.Row)` + grid-template-columns: 1fr 1fr; +`; + +Styled.MigrateAction = styled(Styled.TwoItemRow)` + padding: 1rem; + + background-color: var(--color-layer-2); + border: solid var(--border-width) var(--color-border); + border-radius: 0.75rem; +`; + +Styled.Token = styled(Output)` + font: var(--font-large-book); +`; + +Styled.PanelRow = styled(Styled.Row)` + gap: 1.5rem; + max-width: min(100vw, var(--content-max-width)); + align-items: flex-start; + + > section { + cursor: pointer; + } + + @media ${breakpoints.tablet} { + grid-auto-flow: row; + grid-template-columns: 1fr; + max-width: auto; + } +`; + +Styled.BalancePanelContainer = styled.div` + width: 21.25rem; + + @media ${breakpoints.tablet} { + width: auto; + } +`; + +Styled.Title = styled.h3` + ${layoutMixins.inlineRow} + padding: 1.25rem 1.5rem 0.5rem; + + font: var(--font-medium-book); + color: var(--color-text-2); +`; + +Styled.MigrateTitle = styled(Styled.Title)` + padding: 0 0 0.5rem; +`; + +Styled.MobileMigrateCard = styled(Styled.Panel)` + ${layoutMixins.flexColumn} + gap: 1rem; + + align-items: center; +`; + +Styled.MobileMigrateHeader = styled(Styled.Title)` + ${layoutMixins.inlineRow} + gap: 1ch; + padding-bottom: 1rem; + + font: var(--font-small-book); + color: var(--color-text-0); + + h3 { + ${layoutMixins.inlineRow} + font: var(--font-large-book); + color: var(--color-text-2); + + svg { + font-size: 1.75rem; + } + } + + span { + margin-top: 0.2rem; + b { + font-weight: var(--fontWeight-book); + color: var(--color-text-1); + } + } +`; + +Styled.Details = styled(Details)` + padding: 0.5rem 1rem; +`; + +Styled.WithReceipt = styled(WithReceipt)` + width: 100%; +`; + +Styled.LearnMore = styled(Styled.Description)` + ${layoutMixins.row} + gap: 1ch; +`; + +Styled.IconButton = styled(IconButton)` + color: var(--color-text-0); `; diff --git a/src/state/account.ts b/src/state/account.ts index 467774f..e3178d8 100644 --- a/src/state/account.ts +++ b/src/state/account.ts @@ -37,6 +37,7 @@ export type AccountState = { walletType?: WalletType; historicalPnlPeriod?: HistoricalPnlPeriods; balances?: Record; + stakingBalances?: Record; }; const initialState: AccountState = { @@ -154,6 +155,9 @@ export const accountSlice = createSlice({ setBalances: (state, action: PayloadAction>) => { state.balances = action.payload; }, + setStakingBalances: (state, action: PayloadAction>) => { + state.stakingBalances = action.payload; + }, addUncommittedOrderClientId: (state, action: PayloadAction) => { state.uncommittedOrderClientIds.push(action.payload); }, @@ -179,6 +183,7 @@ export const { viewedFills, viewedOrders, setBalances, + setStakingBalances, addUncommittedOrderClientId, removeUncommittedOrderClientId, } = accountSlice.actions; diff --git a/src/state/accountSelectors.ts b/src/state/accountSelectors.ts index 8054dc3..4a81a5b 100644 --- a/src/state/accountSelectors.ts +++ b/src/state/accountSelectors.ts @@ -337,3 +337,8 @@ export const getUserStats = (state: RootState) => ({ * @returns user wallet balances */ export const getBalances = (state: RootState) => state.account?.balances; + +/** + * @returns user wallet staking balances + * */ +export const getStakingBalances = (state: RootState) => state.account?.stakingBalances; diff --git a/src/views/dialogs/ExternalNavKeplrDialog.tsx b/src/views/dialogs/ExternalNavKeplrDialog.tsx new file mode 100644 index 0000000..505d2a2 --- /dev/null +++ b/src/views/dialogs/ExternalNavKeplrDialog.tsx @@ -0,0 +1,107 @@ +import styled, { type AnyStyledComponent } from 'styled-components'; + +import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; +import { STRING_KEYS } from '@/constants/localization'; +import { useBreakpoints, useStringGetter } from '@/hooks'; + +import { Button } from '@/components/Button'; +import { Dialog, DialogPlacement } from '@/components/Dialog'; +import { IconName } from '@/components/Icon'; +import { IconButton } from '@/components/IconButton'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +type ElementProps = { + setIsOpen: (open: boolean) => void; +}; + +// TODO: replace placeholder URL with real URLs when avaialble +const KEPLR_DASHBOARD_URL = 'https://testnet.keplr.app/'; +const HELP_URL = 'https://help.dydx.exchange/en/articles/2921366-how-do-i-create-an-account-or-sign-up'; + +export const ExternalNavKeplrDialog = ({ setIsOpen }: ElementProps) => { + const stringGetter = useStringGetter(); + const { isTablet } = useBreakpoints(); + + return ( + + + + + {stringGetter({ + key: STRING_KEYS.NAVIGATE_TO_KEPLR, + params: { + STRONG_YES: {stringGetter({ key: STRING_KEYS.YES })}, + }, + })} + + + + + + + + {stringGetter({ + key: STRING_KEYS.LEARN_TO_EXPORT, + params: { + STRONG_NO: {stringGetter({ key: STRING_KEYS.NO })}, + }, + })} + + + + + + + ); +}; + +const Styled: Record = {}; + +Styled.TextToggle = styled.div` + ${layoutMixins.stickyFooter} + color: var(--color-accent); + cursor: pointer; + + margin-top: auto; + + &:hover { + text-decoration: underline; + } +`; + +Styled.Content = styled.div` + ${layoutMixins.stickyArea0} + --stickyArea0-bottomHeight: 2rem; + --stickyArea0-bottomGap: 1rem; + --stickyArea0-totalInsetBottom: 0.5rem; + + ${layoutMixins.flexColumn} + gap: 1rem; +`; + +Styled.Button = styled(Button)` + --button-font: var(--font-base-book); + --button-padding: 0 1.5rem; + + gap: 0; + + justify-content: space-between; +`; + +Styled.IconButton = styled(IconButton)` + color: var(--color-text-0); +`;