Compare commits

...
Author SHA1 Message Date
Aleka Cheung d60d7840b3 localize trading reward strings 2024-01-11 14:47:18 -05:00
Aleka Cheung ce84b4c21b nits + localization 2024-01-11 14:47:16 -05:00
Aleka Cheung 85cf0ecd77 nits + localization 2024-01-11 14:46:37 -05:00
Aleka Cheung 7eae69137b reward history table, polish etc 2024-01-09 17:06:33 -05:00
Aleka Cheung 340e124016 add period 2024-01-09 17:03:41 -05:00
Aleka Cheung a6c1aa5fdc add help panel 2024-01-09 17:03:41 -05:00
Aleka Cheung fc00e1d2a9 set up trading rewards layout and data 2024-01-09 17:03:39 -05:00
20 changed files with 792 additions and 182 deletions
+1 -1
View File
@@ -41,7 +41,7 @@
"@cosmjs/tendermint-rpc": "^0.31.0", "@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/v4-abacus": "^1.1.33", "@dydxprotocol/v4-abacus": "^1.1.33",
"@dydxprotocol/v4-client-js": "^1.0.11", "@dydxprotocol/v4-client-js": "^1.0.11",
"@dydxprotocol/v4-localization": "^1.1.6", "@dydxprotocol/v4-localization": "^1.1.12",
"@ethersproject/providers": "^5.7.2", "@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3", "@js-joda/core": "^5.5.3",
"@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-accordion": "^1.1.2",
+8 -8
View File
@@ -1,9 +1,5 @@
lockfileVersion: '6.0' lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies: dependencies:
'@0xsquid/sdk': '@0xsquid/sdk':
specifier: ^1.10.0 specifier: ^1.10.0
@@ -33,8 +29,8 @@ dependencies:
specifier: ^1.0.11 specifier: ^1.0.11
version: 1.0.11 version: 1.0.11
'@dydxprotocol/v4-localization': '@dydxprotocol/v4-localization':
specifier: ^1.1.6 specifier: ^1.1.12
version: 1.1.6 version: 1.1.12
'@ethersproject/providers': '@ethersproject/providers':
specifier: ^5.7.2 specifier: ^5.7.2
version: 5.7.2 version: 5.7.2
@@ -1023,8 +1019,8 @@ packages:
- utf-8-validate - utf-8-validate
dev: false dev: false
/@dydxprotocol/v4-localization@1.1.6: /@dydxprotocol/v4-localization@1.1.12:
resolution: {integrity: sha512-Bon6NSRU4/FqneAbnP2G28EAPr0hp4LhvAayX61o0O1PGkxnLzAHkXeFppdM0Zn0fOcp1S1MJ+gvz138ZDephQ==} resolution: {integrity: sha512-Mn6Nuosb2nwvxWMviz4emzyoxIOOkI3XJjzateBbOv++tXPxnbLtCLFOU6mMdUk5ZwFnjgb5SEFypXH69cw+xg==}
dev: false dev: false
/@dydxprotocol/v4-proto@0.4.1: /@dydxprotocol/v4-proto@0.4.1:
@@ -14813,3 +14809,7 @@ packages:
/zwitch@2.0.4: /zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: true dev: true
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+10 -1
View File
@@ -65,6 +65,9 @@ type ElementProps = {
resolution?: number; resolution?: number;
stripRelativeWords?: boolean; stripRelativeWords?: boolean;
}; };
timeOptions?: {
useUTC?: boolean;
};
tag?: React.ReactNode; tag?: React.ReactNode;
withParentheses?: boolean; withParentheses?: boolean;
locale?: string; locale?: string;
@@ -89,6 +92,7 @@ export const Output = ({
relativeTimeFormatOptions = { relativeTimeFormatOptions = {
format: 'singleCharacter', format: 'singleCharacter',
}, },
timeOptions,
tag, tag,
withParentheses, withParentheses,
locale = navigator.language || 'en-US', locale = navigator.language || 'en-US',
@@ -165,16 +169,21 @@ export const Output = ({
if ((typeof value !== 'string' && typeof value !== 'number') || !value) return null; if ((typeof value !== 'string' && typeof value !== 'number') || !value) return null;
const date = new Date(value); const date = new Date(value);
const dateString = { const dateString = {
[OutputType.Date]: date.toLocaleString(selectedLocale, { dateStyle: 'medium' }), [OutputType.Date]: date.toLocaleString(selectedLocale, {
dateStyle: 'medium',
timeZone: timeOptions?.useUTC ? 'UTC' : undefined,
}),
[OutputType.DateTime]: date.toLocaleString(selectedLocale, { [OutputType.DateTime]: date.toLocaleString(selectedLocale, {
dateStyle: 'short', dateStyle: 'short',
timeStyle: 'short', timeStyle: 'short',
timeZone: timeOptions?.useUTC ? 'UTC' : undefined,
}), }),
[OutputType.Time]: date.toLocaleString(selectedLocale, { [OutputType.Time]: date.toLocaleString(selectedLocale, {
hour12: false, hour12: false,
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
second: '2-digit', second: '2-digit',
timeZone: timeOptions?.useUTC ? 'UTC' : undefined,
}), }),
}[type]; }[type];
+83 -62
View File
@@ -33,17 +33,19 @@ import {
import { useAsyncList } from 'react-stately'; import { useAsyncList } from 'react-stately';
import { useBreakpoints } from '@/hooks'; import { useBreakpoints, useStringGetter } from '@/hooks';
import { MediaQueryKeys } from '@/hooks/useBreakpoints'; import { MediaQueryKeys } from '@/hooks/useBreakpoints';
import { Checkbox } from '@/components/Checkbox';
import { breakpoints } from '@/styles'; import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { STRING_KEYS } from '@/constants/localization';
import { CaretIcon } from '@/icons';
import { Icon, IconName } from './Icon'; import { Icon, IconName } from './Icon';
import { Tag } from './Tag'; import { Tag } from './Tag';
import { MustBigNumber } from '@/lib/numbers'; import { MustBigNumber } from '@/lib/numbers';
import { Button } from './Button';
export { TableCell } from './Table/TableCell'; export { TableCell } from './Table/TableCell';
export { TableColumnHeader } from './Table/TableColumnHeader'; export { TableColumnHeader } from './Table/TableColumnHeader';
@@ -92,6 +94,7 @@ type ElementProps<TableRowData extends object | CustomRowConfig, TableRowKey ext
selectionBehavior?: 'replace' | 'toggle'; selectionBehavior?: 'replace' | 'toggle';
onRowAction?: (key: TableRowKey, row: TableRowData) => void; onRowAction?: (key: TableRowKey, row: TableRowData) => void;
slotEmpty?: React.ReactNode; slotEmpty?: React.ReactNode;
initialNumRowsToShow?: number;
// collection: TableCollection<string>; // collection: TableCollection<string>;
// children: React.ReactNode; // children: React.ReactNode;
}; };
@@ -121,6 +124,7 @@ export const Table = <TableRowData extends object, TableRowKey extends Key>({
selectionMode = 'single', selectionMode = 'single',
selectionBehavior = 'toggle', selectionBehavior = 'toggle',
slotEmpty, slotEmpty,
initialNumRowsToShow = data.length,
// shouldRowRender, // shouldRowRender,
// collection, // collection,
@@ -136,6 +140,8 @@ export const Table = <TableRowData extends object, TableRowKey extends Key>({
style, style,
}: ElementProps<TableRowData, TableRowKey> & StyleProps) => { }: ElementProps<TableRowData, TableRowKey> & StyleProps) => {
const [selectedKeys, setSelectedKeys] = useState(new Set<TableRowKey>()); const [selectedKeys, setSelectedKeys] = useState(new Set<TableRowKey>());
const [numRowsToShow, setNumRowsToShow] = useState(initialNumRowsToShow);
const stringGetter = useStringGetter();
const currentBreakpoints = useBreakpoints(); const currentBreakpoints = useBreakpoints();
const shownColumns = columns.filter( const shownColumns = columns.filter(
@@ -188,67 +194,77 @@ export const Table = <TableRowData extends object, TableRowKey extends Key>({
const isEmpty = data.length === 0; const isEmpty = data.length === 0;
return ( return (
<Styled.TableWrapper <>
className={className} <Styled.TableWrapper
style={style} className={className}
isEmpty={isEmpty} style={style}
withGradientCardRows={withGradientCardRows} isEmpty={isEmpty}
withOuterBorder={withOuterBorder} withGradientCardRows={withGradientCardRows}
> withOuterBorder={withOuterBorder}
{!isEmpty ? ( >
<TableRoot {!isEmpty ? (
aria-label={label} <TableRoot
sortDescriptor={list.sortDescriptor} aria-label={label}
onSortChange={list.sort} sortDescriptor={list.sortDescriptor}
selectedKeys={selectedKeys} onSortChange={list.sort}
setSelectedKeys={setSelectedKeys} selectedKeys={selectedKeys}
selectionMode={selectionMode} setSelectedKeys={setSelectedKeys}
selectionBehavior={selectionBehavior} selectionMode={selectionMode}
getRowAttributes={getRowAttributes} selectionBehavior={selectionBehavior}
onRowAction={ getRowAttributes={getRowAttributes}
onRowAction && onRowAction={
((key: TableRowKey) => onRowAction(key, data.find((row) => getRowKey(row) === key)!)) onRowAction &&
} ((key: TableRowKey) => onRowAction(key, data.find((row) => getRowKey(row) === key)!))
// shouldRowRender={shouldRowRender} }
hideHeader={hideHeader} // shouldRowRender={shouldRowRender}
withGradientCardRows={withGradientCardRows} hideHeader={hideHeader}
withFocusStickyRows={withFocusStickyRows} withGradientCardRows={withGradientCardRows}
withOuterBorder={withOuterBorder} withFocusStickyRows={withFocusStickyRows}
withInnerBorders={withInnerBorders} withOuterBorder={withOuterBorder}
withScrollSnapColumns={withScrollSnapColumns} withInnerBorders={withInnerBorders}
withScrollSnapRows={withScrollSnapRows} withScrollSnapColumns={withScrollSnapColumns}
> withScrollSnapRows={withScrollSnapRows}
<TableHeader columns={shownColumns}> >
{(column) => ( <TableHeader columns={shownColumns}>
<Column {(column) => (
key={column.columnKey} <Column
childColumns={column.childColumns} key={column.columnKey}
allowsSorting={column.allowsSorting ?? true} childColumns={column.childColumns}
allowsResizing={column.allowsResizing} allowsSorting={column.allowsSorting ?? true}
width={column.width} allowsResizing={column.allowsResizing}
> width={column.width}
{column.label} >
{column.tag && <Tag>{column.tag}</Tag>} {column.label}
</Column> {column.tag && <Tag>{column.tag}</Tag>}
)} </Column>
</TableHeader> )}
</TableHeader>
<TableBody items={list.items}> <TableBody items={list.items.slice(0, numRowsToShow)}>
{(item) => ( {(item) => (
<Row key={getRowKey(item)}> <Row key={getRowKey(item)}>
{(columnKey) => ( {(columnKey) => (
<Cell key={`${getRowKey(item)}-${columnKey}`}> <Cell key={`${getRowKey(item)}-${columnKey}`}>
{columns.find((column) => column.columnKey === columnKey)?.renderCell?.(item)} {columns.find((column) => column.columnKey === columnKey)?.renderCell?.(item)}
</Cell> </Cell>
)} )}
</Row> </Row>
)} )}
</TableBody> </TableBody>
</TableRoot> </TableRoot>
) : ( ) : (
<Styled.Empty withOuterBorder={withOuterBorder}>{slotEmpty}</Styled.Empty> <Styled.Empty withOuterBorder={withOuterBorder}>{slotEmpty}</Styled.Empty>
)}
</Styled.TableWrapper>
{numRowsToShow !== undefined && numRowsToShow < data.length && (
<Styled.ViewMoreButton
onClick={() => setNumRowsToShow(data.length)}
slotRight={<CaretIcon />}
>
{stringGetter({ key: STRING_KEYS.VIEW_MORE })}
</Styled.ViewMoreButton>
)} )}
</Styled.TableWrapper> </>
); );
}; };
@@ -922,3 +938,8 @@ Styled.Row = styled.div`
${layoutMixins.inlineRow} ${layoutMixins.inlineRow}
padding: var(--tableCell-padding); padding: var(--tableCell-padding);
`; `;
Styled.ViewMoreButton = styled(Button)`
--button-backgroundColor: var(--color-layer-2);
width: 100%;
`;
+17
View File
@@ -119,6 +119,13 @@ export const InputSelectionOption = Abacus.exchange.dydx.abacus.output.input.Sel
// ------ Wallet ------ // // ------ Wallet ------ //
export type Wallet = Abacus.exchange.dydx.abacus.output.Wallet; export type Wallet = Abacus.exchange.dydx.abacus.output.Wallet;
export type AccountBalance = Abacus.exchange.dydx.abacus.output.AccountBalance; export type AccountBalance = Abacus.exchange.dydx.abacus.output.AccountBalance;
export type TradingRewards = Abacus.exchange.dydx.abacus.output.TradingRewards;
export type HistoricalTradingReward = Abacus.exchange.dydx.abacus.output.HistoricalTradingReward;
export const HistoricaTradingRewardsPeriod =
Abacus.exchange.dydx.abacus.state.manager.HistoricaTradingRewardsPeriod;
const historicalTradingRewardsPeriod = [...HistoricaTradingRewardsPeriod.values()] as const;
export type HistoricaTradingRewardsPeriods = (typeof historicalTradingRewardsPeriod)[number];
export type Subaccount = Abacus.exchange.dydx.abacus.output.Subaccount; export type Subaccount = Abacus.exchange.dydx.abacus.output.Subaccount;
export type SubaccountPosition = Abacus.exchange.dydx.abacus.output.SubaccountPosition; export type SubaccountPosition = Abacus.exchange.dydx.abacus.output.SubaccountPosition;
export type SubaccountOrder = Abacus.exchange.dydx.abacus.output.SubaccountOrder; export type SubaccountOrder = Abacus.exchange.dydx.abacus.output.SubaccountOrder;
@@ -236,6 +243,16 @@ export const HISTORICAL_PNL_PERIODS: Record<
[HistoricalPnlPeriod.Period90d.name]: HistoricalPnlPeriod.Period90d, [HistoricalPnlPeriod.Period90d.name]: HistoricalPnlPeriod.Period90d,
}; };
export const HISTORICAL_TRADING_REWARDS_PERIODS: Record<
KotlinIrEnumValues<typeof HistoricaTradingRewardsPeriod>,
HistoricaTradingRewardsPeriods
> = {
[HistoricaTradingRewardsPeriod.MONTHLY.name]: HistoricaTradingRewardsPeriod.MONTHLY,
[HistoricaTradingRewardsPeriod.WEEKLY.name]: HistoricaTradingRewardsPeriod.WEEKLY,
[HistoricaTradingRewardsPeriod.DAILY.name]: HistoricaTradingRewardsPeriod.DAILY,
[HistoricaTradingRewardsPeriod.BLOCK.name]: HistoricaTradingRewardsPeriod.BLOCK,
};
export const ORDER_STATUS_STRINGS: Record<KotlinIrEnumValues<typeof AbacusOrderStatus>, string> = { export const ORDER_STATUS_STRINGS: Record<KotlinIrEnumValues<typeof AbacusOrderStatus>, string> = {
[AbacusOrderStatus.open.name]: STRING_KEYS.OPEN_STATUS, [AbacusOrderStatus.open.name]: STRING_KEYS.OPEN_STATUS,
[AbacusOrderStatus.open.rawValue]: STRING_KEYS.OPEN_STATUS, [AbacusOrderStatus.open.rawValue]: STRING_KEYS.OPEN_STATUS,
+12 -5
View File
@@ -9,6 +9,8 @@ import type {
TransferInputFields, TransferInputFields,
HistoricalPnlPeriods, HistoricalPnlPeriods,
ParsingError, ParsingError,
HistoricaTradingRewardsPeriods,
HistoricaTradingRewardsPeriod,
} from '@/constants/abacus'; } from '@/constants/abacus';
import { import {
@@ -218,6 +220,12 @@ class AbacusStateManager {
this.stateManager.historicalPnlPeriod = period; this.stateManager.historicalPnlPeriod = period;
}; };
setHistoricalTradingRewardPeriod = (
period: (typeof HistoricaTradingRewardsPeriod)[keyof typeof HistoricaTradingRewardsPeriod]
) => {
this.stateManager.historicalTradingRewardPeriod = period;
};
switchNetwork = (network: DydxNetwork) => { switchNetwork = (network: DydxNetwork) => {
this.stateManager.environmentId = network; this.stateManager.environmentId = network;
@@ -262,17 +270,16 @@ class AbacusStateManager {
) => this.stateManager.cancelOrder(orderId, callback); ) => this.stateManager.cancelOrder(orderId, callback);
cctpWithdraw = ( cctpWithdraw = (
callback: ( callback: (success: boolean, parsingError: Nullable<ParsingError>, data: string) => void
success: boolean,
parsingError: Nullable<ParsingError>,
data: string,
) => void
): void => this.stateManager.commitCCTPWithdraw(callback); ): void => this.stateManager.commitCCTPWithdraw(callback);
// ------ Utils ------ // // ------ Utils ------ //
getHistoricalPnlPeriod = (): Nullable<HistoricalPnlPeriods> => getHistoricalPnlPeriod = (): Nullable<HistoricalPnlPeriods> =>
this.stateManager.historicalPnlPeriod; this.stateManager.historicalPnlPeriod;
getHistoricalTradingRewardPeriod = (): HistoricaTradingRewardsPeriods =>
this.stateManager.historicalTradingRewardPeriod;
handleCandlesSubscription = ({ handleCandlesSubscription = ({
channelId, channelId,
subscribe, subscribe,
+7
View File
@@ -29,6 +29,7 @@ import {
setSubaccount, setSubaccount,
setTransfers, setTransfers,
setWallet, setWallet,
setTradingRewards,
} from '@/state/account'; } from '@/state/account';
import { setApiState } from '@/state/app'; import { setApiState } from '@/state/app';
@@ -96,6 +97,12 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol {
} }
} }
if (changes.has(Changes.tradingRewards)) {
if (updatedState.account?.tradingRewards) {
dispatch(setTradingRewards(updatedState.account?.tradingRewards));
}
}
if (changes.has(Changes.configs)) { if (changes.has(Changes.configs)) {
dispatch(setConfigs(updatedState.configs)); dispatch(setConfigs(updatedState.configs));
} }
+6 -1
View File
@@ -26,12 +26,13 @@ import { useAccounts, useStringGetter, useTokenConfigs } from '@/hooks';
import { getOnboardingState } from '@/state/accountSelectors'; import { getOnboardingState } from '@/state/accountSelectors';
import { openDialog } from '@/state/dialogs'; import { openDialog } from '@/state/dialogs';
import abacusStateManager from '@/lib/abacus';
import { isTruthy } from '@/lib/isTruthy'; import { isTruthy } from '@/lib/isTruthy';
import { truncateAddress } from '@/lib/wallet'; import { truncateAddress } from '@/lib/wallet';
import { DYDXBalancePanel } from './rewards/DYDXBalancePanel'; import { DYDXBalancePanel } from './rewards/DYDXBalancePanel';
import { MigratePanel } from './rewards/MigratePanel'; import { MigratePanel } from './rewards/MigratePanel';
import { GovernancePanel } from './rewards/GovernancePanel';
import { StakingPanel } from './rewards/StakingPanel';
const ENS_CHAIN_ID = 1; // Ethereum const ENS_CHAIN_ID = 1; // Ethereum
@@ -230,6 +231,9 @@ const Profile = () => {
withInnerBorders={false} withInnerBorders={false}
/> />
</Styled.TablePanel> </Styled.TablePanel>
<GovernancePanel />
<StakingPanel />
</Styled.MobileProfileLayout> </Styled.MobileProfileLayout>
); );
}; };
@@ -243,6 +247,7 @@ Styled.MobileProfileLayout = styled.div`
gap: 1rem; gap: 1rem;
padding: 1.25rem 0.9rem; padding: 1.25rem 0.9rem;
max-width: 100vw;
`; `;
Styled.Header = styled.header` Styled.Header = styled.header`
+79
View File
@@ -0,0 +1,79 @@
import styled, { AnyStyledComponent } from 'styled-components';
import { useDispatch } from 'react-redux';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { DialogTypes } from '@/constants/dialogs';
import { useStringGetter, useURLConfigs } from '@/hooks';
import { Panel } from '@/components/Panel';
import { IconName } from '@/components/Icon';
import { IconButton } from '@/components/IconButton';
import { Link } from '@/components/Link';
import { openDialog } from '@/state/dialogs';
export const GovernancePanel = () => {
const stringGetter = useStringGetter();
const dispatch = useDispatch();
const { governanceLearnMore } = useURLConfigs();
return (
<Styled.Panel
slotHeaderContent={
<Styled.Title>{stringGetter({ key: STRING_KEYS.GOVERNANCE })}</Styled.Title>
}
slotRight={
<Styled.Arrow>
<Styled.IconButton
action={ButtonAction.Base}
iconName={IconName.Arrow}
size={ButtonSize.Small}
/>
</Styled.Arrow>
}
onClick={() => dispatch(openDialog({ type: DialogTypes.ExternalNavKeplr }))}
>
<Styled.Description>
{stringGetter({ key: STRING_KEYS.GOVERNANCE_DESCRIPTION })}
<Link href={governanceLearnMore} onClick={(e) => e.stopPropagation()}>
{stringGetter({ key: STRING_KEYS.LEARN_MORE })}
</Link>
</Styled.Description>
</Styled.Panel>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Panel = styled(Panel)`
height: fit-content;
`;
Styled.Description = styled.div`
color: var(--color-text-0);
--link-color: var(--color-text-1);
a {
display: inline;
::before {
content: ' ';
}
}
`;
Styled.IconButton = styled(IconButton)`
color: var(--color-text-0);
--color-border: var(--color-layer-6);
`;
Styled.Arrow = styled.div`
padding-right: 1.5rem;
`;
Styled.Title = styled.h3`
font: var(--font-medium-book);
color: var(--color-text-2);
margin-bottom: -1rem;
`;
+2 -1
View File
@@ -146,14 +146,15 @@ Styled.Title = styled.h3`
color: var(--color-text-2); color: var(--color-text-2);
padding: var(--panel-paddingY) var(--panel-paddingX) 0; padding: var(--panel-paddingY) var(--panel-paddingX) 0;
margin-bottom: -0.5rem;
`; `;
Styled.MigrateAction = styled.div` Styled.MigrateAction = styled.div`
${layoutMixins.flexEqualColumns} ${layoutMixins.flexEqualColumns}
align-items: center; align-items: center;
margin: 1rem;
gap: 1rem; gap: 1rem;
padding: 1rem; padding: 1rem;
margin: 1rem;
width: 100%; width: 100%;
background-color: var(--color-layer-2); background-color: var(--color-layer-2);
+107
View File
@@ -0,0 +1,107 @@
import { useCallback, useState } from 'react';
import styled, { AnyStyledComponent } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import breakpoints from '@/styles/breakpoints';
import { layoutMixins } from '@/styles/layoutMixins';
import { useStringGetter } from '@/hooks';
import {
HISTORICAL_TRADING_REWARDS_PERIODS,
HistoricaTradingRewardsPeriod,
HistoricaTradingRewardsPeriods,
} from '@/constants/abacus';
import { ComingSoon } from '@/components/ComingSoon';
import { Panel } from '@/components/Panel';
import { ToggleGroup } from '@/components/ToggleGroup';
import { TradingRewardHistoryTable } from '@/views/tables/TradingRewardHistoryTable';
import { getHistoricalTradingRewards } from '@/state/accountSelectors';
import abacusStateManager from '@/lib/abacus';
import { STRING_KEYS } from '@/constants/localization';
export const RewardHistoryPanel = () => {
const stringGetter = useStringGetter();
const historicalTradingRewards = useSelector(getHistoricalTradingRewards, shallowEqual);
const [selectedPeriod, setSelectedPeriod] = useState<HistoricaTradingRewardsPeriods>(
abacusStateManager.getHistoricalTradingRewardPeriod() || HistoricaTradingRewardsPeriod.WEEKLY
);
const onSelectPeriod = useCallback(
(periodName: string) => {
setSelectedPeriod(
HISTORICAL_TRADING_REWARDS_PERIODS[
periodName as keyof typeof HISTORICAL_TRADING_REWARDS_PERIODS
]
);
},
[setSelectedPeriod, selectedPeriod]
);
return (
<Panel
slotHeader={
<Styled.Header>
<Styled.Title>
<h3>{stringGetter({ key: STRING_KEYS.REWARD_HISTORY })}</h3>
<span>{stringGetter({ key: STRING_KEYS.REWARD_HISTORY_DESCRIPTION })}</span>
</Styled.Title>
<ToggleGroup
items={[
{
value: HistoricaTradingRewardsPeriod.MONTHLY.name,
label: stringGetter({ key: STRING_KEYS.MONTHLY }),
},
{
value: HistoricaTradingRewardsPeriod.WEEKLY.name,
label: stringGetter({ key: STRING_KEYS.WEEKLY }),
},
{
value: HistoricaTradingRewardsPeriod.DAILY.name,
label: stringGetter({ key: STRING_KEYS.DAILY }),
},
]}
value={selectedPeriod.name}
onValueChange={onSelectPeriod}
/>
</Styled.Header>
}
>
{historicalTradingRewards ? (
<TradingRewardHistoryTable period={selectedPeriod} />
) : (
<ComingSoon />
)}
</Panel>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Header = styled.div`
${layoutMixins.spacedRow}
padding: 1rem 1rem 0;
margin-bottom: -0.5rem;
@media ${breakpoints.notTablet} {
padding: 1.25rem 1.5rem 0;
}
`;
Styled.Title = styled.div`
color: var(--color-text-0);
font: var(--font-small-book);
h3 {
font: var(--font-medium-book);
color: var(--color-text-2);
}
`;
Styled.Content = styled.div`
${layoutMixins.flexColumn}
gap: 0.75rem;
`;
+6 -8
View File
@@ -30,18 +30,16 @@ export const RewardsHelpPanel = () => {
<Accordion <Accordion
items={[ items={[
{ {
header: 'Who is eligible for trading rewards?', header: stringGetter({ key: STRING_KEYS.FAQ_WHO_IS_ELIGIBLE_QUESTION }),
content: 'All traders are eligible for trading rewards.', content: stringGetter({ key: STRING_KEYS.FAQ_WHO_IS_ELIGIBLE_ANSWER }),
}, },
{ {
header: 'How do trading rewards work?', header: stringGetter({ key: STRING_KEYS.FAQ_HOW_DO_TRADING_REWARDS_WORK_QUESTION }),
content: content: stringGetter({ key: STRING_KEYS.FAQ_HOW_DO_TRADING_REWARDS_WORK_ANSWER }),
'Immediately after each fill, trading rewards are sent directly to the traders dYdX Chain address, based on the amount of fees paid by the trader.',
}, },
{ {
header: 'How do I claim my rewards?', header: stringGetter({ key: STRING_KEYS.FAQ_HOW_DO_I_CLAIM_MY_REWARDS_QUESTION }),
content: content: stringGetter({ key: STRING_KEYS.FAQ_HOW_DO_I_CLAIM_MY_REWARDS_ANSWER }),
'Each block, trading rewards are automatically sent directly to the traders dYdX Chain address.',
}, },
]} ]}
/> />
+69 -91
View File
@@ -1,13 +1,11 @@
import styled, { AnyStyledComponent } from 'styled-components'; import styled, { AnyStyledComponent } from 'styled-components';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { ButtonAction, ButtonSize } from '@/constants/buttons'; import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
import { AppRoute } from '@/constants/routes'; import { AppRoute } from '@/constants/routes';
import { useBreakpoints, useStringGetter, useURLConfigs } from '@/hooks'; import { useBreakpoints, useStringGetter } from '@/hooks';
import { breakpoints } from '@/styles'; import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
@@ -16,32 +14,21 @@ import { BackButton } from '@/components/BackButton';
import { Panel } from '@/components/Panel'; import { Panel } from '@/components/Panel';
import { IconName } from '@/components/Icon'; import { IconName } from '@/components/Icon';
import { IconButton } from '@/components/IconButton'; import { IconButton } from '@/components/IconButton';
import { Link } from '@/components/Link';
import { openDialog } from '@/state/dialogs';
import { DYDXBalancePanel } from './DYDXBalancePanel'; import { DYDXBalancePanel } from './DYDXBalancePanel';
import { MigratePanel } from './MigratePanel'; import { MigratePanel } from './MigratePanel';
import { LaunchIncentivesPanel } from './LaunchIncentivesPanel'; import { LaunchIncentivesPanel } from './LaunchIncentivesPanel';
import { RewardsHelpPanel } from './RewardsHelpPanel'; import { RewardsHelpPanel } from './RewardsHelpPanel';
import { TradingRewardsSummaryPanel } from './TradingRewardsSummaryPanel';
import { RewardHistoryPanel } from './RewardHistoryPanel';
import { GovernancePanel } from './GovernancePanel';
import { StakingPanel } from './StakingPanel';
const RewardsPage = () => { const RewardsPage = () => {
const dispatch = useDispatch();
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const { governanceLearnMore, stakingLearnMore } = useURLConfigs();
const { isTablet, isNotTablet } = useBreakpoints(); const { isTablet, isNotTablet } = useBreakpoints();
const navigate = useNavigate(); const navigate = useNavigate();
const panelArrow = (
<Styled.Arrow>
<Styled.IconButton
action={ButtonAction.Base}
iconName={IconName.Arrow}
size={ButtonSize.Small}
/>
</Styled.Arrow>
);
return ( return (
<Styled.Page> <Styled.Page>
{isTablet && ( {isTablet && (
@@ -50,50 +37,32 @@ const RewardsPage = () => {
{stringGetter({ key: STRING_KEYS.TRADING_REWARDS })} {stringGetter({ key: STRING_KEYS.TRADING_REWARDS })}
</Styled.MobileHeader> </Styled.MobileHeader>
)} )}
{import.meta.env.VITE_V3_TOKEN_ADDRESS && isNotTablet && <MigratePanel />} <Styled.GridLayout>
{import.meta.env.VITE_V3_TOKEN_ADDRESS && isNotTablet && <Styled.MigratePanel />}
{isTablet ? ( {isTablet ? (
<LaunchIncentivesPanel /> <Styled.LaunchIncentivesPanel />
) : ( ) : (
<Styled.PanelRowIncentivesAndBalance> <>
<LaunchIncentivesPanel /> <Styled.LaunchIncentivesPanel />
<DYDXBalancePanel /> <Styled.DYDXBalancePanel />
</Styled.PanelRowIncentivesAndBalance> </>
)} )}
<Styled.PanelRow> <Styled.TradingRewardsColumn>
<Styled.Panel <TradingRewardsSummaryPanel />
slotHeaderContent={ {isTablet && <RewardsHelpPanel />}
<Styled.Title>{stringGetter({ key: STRING_KEYS.GOVERNANCE })}</Styled.Title> <RewardHistoryPanel />
} </Styled.TradingRewardsColumn>
slotRight={panelArrow}
onClick={() => dispatch(openDialog({ type: DialogTypes.ExternalNavKeplr }))}
>
<Styled.Description>
{stringGetter({ key: STRING_KEYS.GOVERNANCE_DESCRIPTION })}
<Link href={governanceLearnMore} onClick={(e) => e.stopPropagation()}>
{stringGetter({ key: STRING_KEYS.LEARN_MORE })}
</Link>
</Styled.Description>
</Styled.Panel>
<Styled.Panel {isNotTablet && (
slotHeaderContent={ <Styled.OtherColumn>
<Styled.Title>{stringGetter({ key: STRING_KEYS.STAKING })}</Styled.Title> <GovernancePanel />
} <StakingPanel />
slotRight={panelArrow} <RewardsHelpPanel />
onClick={() => dispatch(openDialog({ type: DialogTypes.ExternalNavKeplr }))} </Styled.OtherColumn>
> )}
<Styled.Description> </Styled.GridLayout>
{stringGetter({ key: STRING_KEYS.STAKING_DESCRIPTION })}
<Link href={stakingLearnMore} onClick={(e) => e.stopPropagation()}>
{stringGetter({ key: STRING_KEYS.LEARN_MORE })}
</Link>
</Styled.Description>
</Styled.Panel>
</Styled.PanelRow>
<RewardsHelpPanel />
</Styled.Page> </Styled.Page>
); );
}; };
@@ -104,7 +73,6 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.Page = styled.div` Styled.Page = styled.div`
${layoutMixins.contentContainerPage} ${layoutMixins.contentContainerPage}
gap: 1.5rem;
padding: 2rem; padding: 2rem;
align-items: center; align-items: center;
@@ -129,54 +97,64 @@ Styled.MobileHeader = styled.header`
${layoutMixins.stickyHeader} ${layoutMixins.stickyHeader}
z-index: 2; z-index: 2;
padding: 1.25rem 0; padding: 1.25rem 0;
margin-bottom: -1.5rem;
font: var(--font-large-medium); font: var(--font-large-medium);
color: var(--color-text-2); color: var(--color-text-2);
background-color: var(--color-layer-2); background-color: var(--color-layer-2);
`; `;
Styled.Panel = styled(Panel)` Styled.GridLayout = styled.div`
height: fit-content; --gap: 1.5rem;
`; display: grid;
grid-template-columns: 2fr 1fr;
gap: var(--gap);
Styled.Title = styled.h3` > * {
font: var(--font-medium-book); gap: var(--gap);
color: var(--color-text-2);
margin-bottom: -1rem;
`;
Styled.Description = styled.div`
color: var(--color-text-0);
--link-color: var(--color-text-1);
a {
display: inline;
::before {
content: ' ';
}
} }
`;
Styled.PanelRow = styled.div` grid-template-areas:
${layoutMixins.gridEqualColumns} 'migrate migrate'
gap: 1.5rem; 'incentives balance'
'rewards other';
@media ${breakpoints.tablet} { @media ${breakpoints.tablet} {
grid-auto-flow: row; --gap: 1rem;
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-areas:
'incentives'
'rewards';
} }
`; `;
Styled.PanelRowIncentivesAndBalance = styled(Styled.PanelRow)` Styled.MigratePanel = styled(MigratePanel)`
grid-template-columns: 2fr 1fr; grid-area: migrate;
`; `;
Styled.IconButton = styled(IconButton)` Styled.LaunchIncentivesPanel = styled(LaunchIncentivesPanel)`
color: var(--color-text-0); grid-area: incentives;
--color-border: var(--color-layer-6);
`; `;
Styled.Arrow = styled.div` Styled.DYDXBalancePanel = styled(DYDXBalancePanel)`
padding: 1rem; grid-area: balance;
`;
Styled.TradingRewardsColumn = styled.div`
grid-area: rewards;
${layoutMixins.flexColumn}
`;
Styled.OtherColumn = styled.div`
grid-area: other;
${layoutMixins.flexColumn}
`;
Styled.RewardHistoryHeader = styled.div`
h3 {
font: var(--font-medium-book);
color: var(--color-text-2);
}
padding: 1rem 1.5rem 0;
margin-bottom: -0.5rem;
`; `;
+77
View File
@@ -0,0 +1,77 @@
import styled, { AnyStyledComponent } from 'styled-components';
import { useDispatch } from 'react-redux';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { DialogTypes } from '@/constants/dialogs';
import { useStringGetter, useURLConfigs } from '@/hooks';
import { Panel } from '@/components/Panel';
import { IconName } from '@/components/Icon';
import { IconButton } from '@/components/IconButton';
import { Link } from '@/components/Link';
import { openDialog } from '@/state/dialogs';
export const StakingPanel = () => {
const stringGetter = useStringGetter();
const dispatch = useDispatch();
const { stakingLearnMore } = useURLConfigs();
return (
<Styled.Panel
slotHeaderContent={<Styled.Title>{stringGetter({ key: STRING_KEYS.STAKING })}</Styled.Title>}
slotRight={
<Styled.Arrow>
<Styled.IconButton
action={ButtonAction.Base}
iconName={IconName.Arrow}
size={ButtonSize.Small}
/>
</Styled.Arrow>
}
onClick={() => dispatch(openDialog({ type: DialogTypes.ExternalNavKeplr }))}
>
<Styled.Description>
{stringGetter({ key: STRING_KEYS.STAKING_DESCRIPTION })}
<Link href={stakingLearnMore} onClick={(e) => e.stopPropagation()}>
{stringGetter({ key: STRING_KEYS.LEARN_MORE })}
</Link>
</Styled.Description>
</Styled.Panel>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Panel = styled(Panel)`
height: fit-content;
`;
Styled.Description = styled.div`
color: var(--color-text-0);
--link-color: var(--color-text-1);
a {
display: inline;
::before {
content: ' ';
}
}
`;
Styled.IconButton = styled(IconButton)`
color: var(--color-text-0);
--color-border: var(--color-layer-6);
`;
Styled.Arrow = styled.div`
padding-right: 1.5rem;
`;
Styled.Title = styled.h3`
font: var(--font-medium-book);
color: var(--color-text-2);
margin-bottom: -1rem;
`;
@@ -0,0 +1,133 @@
import styled, { AnyStyledComponent } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import { STRING_KEYS } from '@/constants/localization';
import { layoutMixins } from '@/styles/layoutMixins';
import { useStringGetter, useTokenConfigs } from '@/hooks';
import { AssetIcon } from '@/components/AssetIcon';
import { ComingSoon } from '@/components/ComingSoon';
import { Details } from '@/components/Details';
import { Output, OutputType } from '@/components/Output';
import { Panel } from '@/components/Panel';
import { getHistoricalTradingRewards } from '@/state/accountSelectors';
export const TradingRewardsSummaryPanel = () => {
const stringGetter = useStringGetter();
const historicalTradingRewards = useSelector(getHistoricalTradingRewards, shallowEqual);
const currentWeekTradingReward = historicalTradingRewards?.get('WEEKLY')?.firstOrNull();
const { chainTokenLabel } = useTokenConfigs();
return (
<Panel
slotHeader={
<Styled.Header>{stringGetter({ key: STRING_KEYS.TRADING_REWARDS_SUMMARY })}</Styled.Header>
}
>
<Styled.Content>
{currentWeekTradingReward ? (
<Styled.TradingRewardsDetails
layout="grid"
items={[
{
key: 'week',
label: (
<Styled.Label>
<h4>{stringGetter({ key: STRING_KEYS.THIS_WEEK })}</h4>
</Styled.Label>
),
value: (
<Styled.Column>
<Output
slotRight={<Styled.AssetIcon symbol={chainTokenLabel} />}
type={OutputType.Asset}
value={currentWeekTradingReward.amount}
/>
<Styled.TimePeriod>
<Output
type={OutputType.Date}
value={currentWeekTradingReward.startedAtInMilliseconds}
timeOptions={{ useUTC: true }}
/>
<Output
type={OutputType.Date}
value={currentWeekTradingReward.endedAtInMilliseconds}
timeOptions={{ useUTC: true }}
/>
</Styled.TimePeriod>
</Styled.Column>
),
},
// TODO(@aforaleka): add all-time when supported
]}
/>
) : (
<ComingSoon />
)}
</Styled.Content>
</Panel>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Header = styled.div`
padding: var(--panel-paddingY) var(--panel-paddingX) 0;
font: var(--font-medium-book);
color: var(--color-text-2);
`;
Styled.Content = styled.div`
${layoutMixins.flexColumn}
gap: 0.75rem;
`;
Styled.TradingRewardsDetails = styled(Details)`
--details-item-backgroundColor: var(--color-layer-6);
grid-template-columns: 1fr; // TODO(@aforaleka): change to 1fr 1fr when all-time is supported
gap: 1rem;
> div {
gap: 0.5rem;
padding: 1rem;
border-radius: 0.75em;
background-color: var(--color-layer-5);
}
dt {
width: 100%;
}
output {
color: var(--color-text-2);
font: var(--font-large-book);
}
`;
Styled.Label = styled.div`
${layoutMixins.spacedRow}
font: var(--font-base-book);
color: var(--color-text-1);
`;
Styled.TimePeriod = styled.div`
${layoutMixins.inlineRow}
&, output {
color: var(--color-text-0);
font: var(--font-small-book);
}
`;
Styled.Column = styled.div`
${layoutMixins.flexColumn}
gap: 0.33rem;
`;
Styled.AssetIcon = styled(AssetIcon)`
margin-left: 0.5ch;
`;
+6
View File
@@ -13,6 +13,7 @@ import type {
HistoricalPnlPeriods, HistoricalPnlPeriods,
SubAccountHistoricalPNLs, SubAccountHistoricalPNLs,
UsageRestriction, UsageRestriction,
TradingRewards,
} from '@/constants/abacus'; } from '@/constants/abacus';
import { OnboardingGuard, OnboardingState } from '@/constants/account'; import { OnboardingGuard, OnboardingState } from '@/constants/account';
@@ -24,6 +25,7 @@ import { getLocalStorage } from '@/lib/localStorage';
export type AccountState = { export type AccountState = {
balances?: Record<string, AccountBalance>; balances?: Record<string, AccountBalance>;
stakingBalances?: Record<string, AccountBalance>; stakingBalances?: Record<string, AccountBalance>;
tradingRewards?: TradingRewards;
wallet?: Nullable<Wallet>; wallet?: Nullable<Wallet>;
walletType?: WalletType; walletType?: WalletType;
@@ -179,6 +181,9 @@ export const accountSlice = createSlice({
setStakingBalances: (state, action: PayloadAction<Record<string, AccountBalance>>) => { setStakingBalances: (state, action: PayloadAction<Record<string, AccountBalance>>) => {
state.stakingBalances = action.payload; state.stakingBalances = action.payload;
}, },
setTradingRewards: (state, action: PayloadAction<TradingRewards>) => {
state.tradingRewards = action.payload;
},
addUncommittedOrderClientId: (state, action: PayloadAction<number>) => { addUncommittedOrderClientId: (state, action: PayloadAction<number>) => {
state.uncommittedOrderClientIds.push(action.payload); state.uncommittedOrderClientIds.push(action.payload);
}, },
@@ -206,6 +211,7 @@ export const {
viewedOrders, viewedOrders,
setBalances, setBalances,
setStakingBalances, setStakingBalances,
setTradingRewards,
addUncommittedOrderClientId, addUncommittedOrderClientId,
removeUncommittedOrderClientId, removeUncommittedOrderClientId,
} = accountSlice.actions; } = accountSlice.actions;
+11
View File
@@ -343,6 +343,17 @@ export const getBalances = (state: RootState) => state.account?.balances;
* */ * */
export const getStakingBalances = (state: RootState) => state.account?.stakingBalances; export const getStakingBalances = (state: RootState) => state.account?.stakingBalances;
/**
* @returns account all time trading rewards
*/
export const getTotalTradingRewards = (state: RootState) => state.account?.tradingRewards?.total;
/**
* @returns account trading rewards aggregated by period
*/
export const getHistoricalTradingRewards = (state: RootState) =>
state.account?.tradingRewards?.historical;
/** /**
* @returns UsageRestriction of the current session * @returns UsageRestriction of the current session
*/ */
@@ -0,0 +1,154 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import { HistoricaTradingRewardsPeriods } from '@/constants/abacus';
import { STRING_KEYS, StringGetterFunction } from '@/constants/localization';
import { useStringGetter, useTokenConfigs } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { AssetIcon } from '@/components/AssetIcon';
import { Output, OutputType } from '@/components/Output';
import { Table, TableCell, type ColumnDef } from '@/components/Table';
import { getHistoricalTradingRewards } from '@/state/accountSelectors';
export enum TradingRewardHistoryTableColumnKey {
Event = 'Event',
Earned = 'Earned',
}
const getTradingRewardHistoryTableColumnDef = ({
key,
chainTokenLabel,
stringGetter,
}: {
key: TradingRewardHistoryTableColumnKey;
chainTokenLabel: string;
stringGetter: StringGetterFunction;
}): ColumnDef<any> => ({
...(
{
[TradingRewardHistoryTableColumnKey.Event]: {
columnKey: TradingRewardHistoryTableColumnKey.Event,
getCellValue: (row) => row.startedAtInMilliseconds,
label: 'Event',
renderCell: ({ startedAtInMilliseconds, endedAtInMilliseconds }) => (
<TableCell stacked>
<Styled.Rewarded>{stringGetter({ key: STRING_KEYS.REWARDED })}</Styled.Rewarded>
<Styled.TimePeriod>
{stringGetter({
key: STRING_KEYS.FOR_TRADING,
params: {
PERIOD: (
<>
<Output
type={OutputType.Date}
value={startedAtInMilliseconds}
timeOptions={{ useUTC: true }}
/>
<Output
type={OutputType.Date}
value={endedAtInMilliseconds}
timeOptions={{ useUTC: true }}
/>
</>
),
},
})}
</Styled.TimePeriod>
</TableCell>
),
},
[TradingRewardHistoryTableColumnKey.Earned]: {
columnKey: TradingRewardHistoryTableColumnKey.Earned,
getCellValue: (row) => row.amount,
label: stringGetter({ key: STRING_KEYS.EARNED }),
renderCell: ({ amount }) => (
<Output
type={OutputType.Asset}
value={amount}
slotRight={<Styled.AssetIcon symbol={chainTokenLabel} />}
/>
),
},
} as Record<TradingRewardHistoryTableColumnKey, ColumnDef<any>>
)[key],
});
type ElementProps = {
columnKeys?: TradingRewardHistoryTableColumnKey[];
period: HistoricaTradingRewardsPeriods;
};
type StyleProps = {
withOuterBorder?: boolean;
withInnerBorders?: boolean;
};
export const TradingRewardHistoryTable = ({
period,
columnKeys = Object.values(TradingRewardHistoryTableColumnKey),
withOuterBorder,
withInnerBorders = true,
}: ElementProps & StyleProps) => {
const stringGetter = useStringGetter();
const historicalTradingRewards = useSelector(getHistoricalTradingRewards, shallowEqual);
const periodTradingRewards = historicalTradingRewards?.get(period.name) ?? [];
const { chainTokenLabel } = useTokenConfigs();
return (
<Styled.Table
label="Reward History"
data={periodTradingRewards}
getRowKey={(row: any) => row.startedAtInMilliseconds}
columns={columnKeys.map((key: TradingRewardHistoryTableColumnKey) =>
getTradingRewardHistoryTableColumnDef({
key,
chainTokenLabel,
stringGetter,
})
)}
selectionBehavior="replace"
withOuterBorder={withOuterBorder}
withInnerBorders={withInnerBorders}
initialNumRowsToShow={5}
withScrollSnapColumns
withScrollSnapRows
/>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Table = styled(Table)`
--tableCell-padding: 0.5rem 0;
--tableHeader-backgroundColor: var(--color-layer-3);
--tableRow-backgroundColor: var(--color-layer-3);
tbody {
font: var(--font-medium-book);
}
`;
Styled.Rewarded = styled.span`
color: var(--color-text-2);
`;
Styled.TimePeriod = styled.div`
${layoutMixins.inlineRow}
&& {
color: var(--color-text-0);
font: var(--font-base-book);
}
output {
color: var(--color-text-1);
font: var(--font-base-book);
}
`;
Styled.AssetIcon = styled(AssetIcon)`
margin-left: 0.5ch;
`;