diff --git a/src/constants/analytics.ts b/src/constants/analytics.ts index 66e1715a0..b4a474675 100644 --- a/src/constants/analytics.ts +++ b/src/constants/analytics.ts @@ -128,6 +128,9 @@ export const AnalyticsEvents = unionize( ExportTransfersCheckboxClick: ofType<{ value: boolean; }>(), + ExportVaultTransfersCheckboxClick: ofType<{ + value: boolean; + }>(), // Navigation NavigatePage: ofType<{ diff --git a/src/constants/routes.ts b/src/constants/routes.ts index d1c8abdaa..585308516 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -29,6 +29,7 @@ export enum PortfolioRoute { export enum HistoryRoute { Trades = 'trades', Transfers = 'transfers', + VaultTransfers = 'vault-transfers', Payments = 'payments', } diff --git a/src/pages/portfolio/History.tsx b/src/pages/portfolio/History.tsx index 248a4cb5c..505ca1942 100644 --- a/src/pages/portfolio/History.tsx +++ b/src/pages/portfolio/History.tsx @@ -13,9 +13,13 @@ import { AttachedExpandingSection } from '@/components/ContentSection'; import { NavigationMenu } from '@/components/NavigationMenu'; import { ExportHistoryDropdown } from '@/views/ExportHistoryDropdown'; +import { isTruthy } from '@/lib/isTruthy'; +import { testFlags } from '@/lib/testFlags'; + export const History = () => { const stringGetter = useStringGetter(); const { isNotTablet } = useBreakpoints(); + const { enableVaults } = testFlags; return ( @@ -38,13 +42,19 @@ export const History = () => { href: HistoryRoute.Transfers, tag: 'USDC', }, + enableVaults && { + value: HistoryRoute.VaultTransfers, + label:

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

, + href: HistoryRoute.VaultTransfers, + tag: 'USDC', + }, // TODO - TRCL-1693 - // { // value: HistoryRoute.Payments, // label:

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

, // href: HistoryRoute.Payments, // }, - ], + ].filter(isTruthy), }, ]} /> diff --git a/src/pages/portfolio/Portfolio.tsx b/src/pages/portfolio/Portfolio.tsx index a03af9305..58eb3fe36 100644 --- a/src/pages/portfolio/Portfolio.tsx +++ b/src/pages/portfolio/Portfolio.tsx @@ -34,6 +34,7 @@ import { openDialog } from '@/state/dialogs'; import { shortenNumberForDisplay } from '@/lib/numbers'; +import { VaultTransactionsTable } from '../vaults/VaultTransactions'; import { PortfolioNavMobile } from './PortfolioNavMobile'; const Overview = lazy(() => import('./Overview').then((module) => ({ default: module.Overview }))); @@ -114,6 +115,15 @@ const PortfolioPage = () => { /> } /> + + } + /> {/* TODO - TRCL-1693 { const stringGetter = useStringGetter(); const { pathname } = useLocation(); const navigate = useNavigate(); + const { enableVaults } = testFlags; const portfolioRouteItems = [ { @@ -51,13 +55,18 @@ export const PortfolioNavMobile = () => { label: stringGetter({ key: STRING_KEYS.TRANSFERS }), description: stringGetter({ key: STRING_KEYS.TRANSFERS_DESCRIPTION }), }, + enableVaults && { + value: `${AppRoute.Portfolio}/${PortfolioRoute.History}/${HistoryRoute.VaultTransfers}`, + label: stringGetter({ key: STRING_KEYS.MEGAVAULT_TRANSFERS }), + description: stringGetter({ key: STRING_KEYS.MEGAVAULT_TRANSFERS_DESCRIPTION }), + }, // TODO: TRCL-1693 - re-enable when Payments are ready // { // value: `${AppRoute.Portfolio}/${PortfolioRoute.History}/${HistoryRoute.Payments}`, // label: stringGetter({ key: STRING_KEYS.PAYMENTS }), // description: stringGetter({ key: STRING_KEYS.PAYMENTS_DESCRIPTION }), // }, - ]; + ].filter(isTruthy); const routeMap = Object.fromEntries( portfolioRouteItems.map(({ value, label }) => [value, { value, label }]) diff --git a/src/pages/vaults/VaultTransactions.tsx b/src/pages/vaults/VaultTransactions.tsx index f1e98ba4e..84c1e20a2 100644 --- a/src/pages/vaults/VaultTransactions.tsx +++ b/src/pages/vaults/VaultTransactions.tsx @@ -55,7 +55,15 @@ export const VaultTransactionsCard = ({ className }: { className?: string }) => ); }; const $ShowHideHistoryButton = styled(Button)``; -const VaultTransactionsTable = ({ className }: { className?: string }) => { +export const VaultTransactionsTable = ({ + className, + withOuterBorders, + emptyString, +}: { + className?: string; + withOuterBorders?: boolean; + emptyString?: string; +}) => { const stringGetter = useStringGetter(); const transactions = useLoadedVaultAccountTransfers() ?? EMPTY_ARR; @@ -117,7 +125,8 @@ const VaultTransactionsTable = ({ className }: { className?: string }) => { }} columns={columns} className={className} - withOuterBorder={transactions.length === 0} + withOuterBorder={transactions.length === 0 || withOuterBorders} + slotEmpty={emptyString} /> ); }; diff --git a/src/views/ExportHistoryDropdown.tsx b/src/views/ExportHistoryDropdown.tsx index a11bea0d4..9887c3bbc 100644 --- a/src/views/ExportHistoryDropdown.tsx +++ b/src/views/ExportHistoryDropdown.tsx @@ -2,6 +2,7 @@ import { useCallback, useState } from 'react'; import { OrderSide } from '@dydxprotocol/v4-client-js'; import { useMutation } from '@tanstack/react-query'; +import { isEmpty } from 'lodash'; import { AnalyticsEvents } from '@/constants/analytics'; import { ButtonAction, ButtonSize } from '@/constants/buttons'; @@ -11,6 +12,7 @@ import { useAccounts } from '@/hooks/useAccounts'; import { useDydxClient } from '@/hooks/useDydxClient'; import { useLocaleSeparators } from '@/hooks/useLocaleSeparators'; import { useStringGetter } from '@/hooks/useStringGetter'; +import { useLoadedVaultAccountTransfers } from '@/hooks/vaultsHooks'; import { Button } from '@/components/Button'; import { Checkbox } from '@/components/Checkbox'; @@ -24,7 +26,9 @@ import { getSelectedLocale } from '@/state/localizationSelectors'; import { track } from '@/lib/analytics/analytics'; import { exportCSV } from '@/lib/csv'; +import { isTruthy } from '@/lib/isTruthy'; import { MustBigNumber } from '@/lib/numbers'; +import { testFlags } from '@/lib/testFlags'; interface ExportHistoryDropdownProps { className?: string; @@ -39,6 +43,7 @@ export const ExportHistoryDropdown = (props: ExportHistoryDropdownProps) => { const { requestAllAccountFills, requestAllAccountTransfers } = useDydxClient(); const [checkedTrades, setCheckedTrades] = useState(true); const [checkedTransfers, setCheckedTransfers] = useState(true); + const [checkedVaultTransfers, setCheckedVaultTransfers] = useState(false); const { decimal: LOCALE_DECIMAL_SEPARATOR, group: LOCALE_GROUP_SEPARATOR } = useLocaleSeparators(); @@ -210,6 +215,62 @@ export const ExportHistoryDropdown = (props: ExportHistoryDropdownProps) => { selectedLocale, ]); + const allVaultTransfers = useLoadedVaultAccountTransfers(); + const exportVaultTransfers = useCallback(async () => { + if (dydxAddress && subaccountNumber !== undefined && allVaultTransfers != null) { + const csvTransfers = allVaultTransfers.map((transfer) => { + const amount = formatNumberOutput(transfer.amountUsdc, OutputType.Fiat, { + decimalSeparator: LOCALE_DECIMAL_SEPARATOR, + groupSeparator: LOCALE_GROUP_SEPARATOR, + selectedLocale, + }); + + return { + time: + transfer.timestampMs == null + ? '' + : new Date(transfer.timestampMs).toLocaleString(selectedLocale, { + dateStyle: 'short', + timeStyle: 'short', + }), + action: transfer.type?.name ?? '', + amount, + id: transfer.id, + }; + }); + + exportCSV(csvTransfers, { + filename: 'vault-transfers', + columnHeaders: [ + { + key: 'time', + displayLabel: stringGetter({ key: STRING_KEYS.TIME }), + }, + { + key: 'action', + displayLabel: stringGetter({ key: STRING_KEYS.ACTION }), + }, + { + key: 'amount', + displayLabel: stringGetter({ key: STRING_KEYS.AMOUNT }), + }, + { + key: 'id', + displayLabel: stringGetter({ key: STRING_KEYS.TRANSACTION }), + }, + ], + }); + } + }, [ + dydxAddress, + subaccountNumber, + allVaultTransfers, + stringGetter, + LOCALE_DECIMAL_SEPARATOR, + LOCALE_GROUP_SEPARATOR, + selectedLocale, + ]); + const { mutate: mutateExportTrades, isPending: isPendingExportTrades } = useMutation({ mutationFn: exportTrades, }); @@ -218,6 +279,11 @@ export const ExportHistoryDropdown = (props: ExportHistoryDropdownProps) => { mutationFn: exportTransfers, }); + const { mutate: mutateExportVaultTransfers, isPending: isPendingExportVaultTransfers } = + useMutation({ + mutationFn: exportVaultTransfers, + }); + const exportData = useCallback( (e: Event) => { e.preventDefault(); @@ -230,6 +296,10 @@ export const ExportHistoryDropdown = (props: ExportHistoryDropdownProps) => { mutateExportTransfers(); } + if (checkedVaultTransfers) { + mutateExportVaultTransfers(); + } + track( AnalyticsEvents.ExportDownloadClick({ trades: checkedTrades, @@ -237,7 +307,14 @@ export const ExportHistoryDropdown = (props: ExportHistoryDropdownProps) => { }) ); }, - [checkedTrades, checkedTransfers, mutateExportTrades, mutateExportTransfers] + [ + checkedTrades, + checkedTransfers, + checkedVaultTransfers, + mutateExportTrades, + mutateExportTransfers, + mutateExportVaultTransfers, + ] ); return ( @@ -261,7 +338,7 @@ export const ExportHistoryDropdown = (props: ExportHistoryDropdownProps) => { /> ), value: 'trades', - onSelect: (e) => e.preventDefault(), + onSelect: (e: Event) => e.preventDefault(), }, { label: ( @@ -276,14 +353,41 @@ export const ExportHistoryDropdown = (props: ExportHistoryDropdownProps) => { /> ), value: 'transfers', - onSelect: (e) => e.preventDefault(), + onSelect: (e: Event) => e.preventDefault(), + }, + testFlags.enableVaults && { + label: ( + { + setCheckedVaultTransfers(!checkedVaultTransfers); + + track( + AnalyticsEvents.ExportVaultTransfersCheckboxClick({ + value: !checkedVaultTransfers, + }) + ); + }} + /> + ), + value: 'vault-transfers', + onSelect: (e: Event) => e.preventDefault(), }, { label: (