From 2d0a944097cda50f226e7644c7ea017182e69618 Mon Sep 17 00:00:00 2001 From: Aleka Cheung Date: Mon, 21 Aug 2023 17:17:48 -0400 Subject: [PATCH] transfers history table --- src/constants/abacus.ts | 2 + src/constants/localization/app.ts | 2 + src/lib/abacus/stateNotification.ts | 6 + src/localization/en/app.json | 2 + src/pages/portfolio/History.tsx | 11 +- src/pages/portfolio/Portfolio.tsx | 8 +- src/pages/portfolio/PortfolioNavMobile.tsx | 12 +- src/state/account.ts | 7 + src/state/accountSelectors.ts | 6 + src/styles/tradeViewMixins.ts | 4 + src/views/tables/TransferHistoryTable.tsx | 184 +++++++++++++++++++++ 11 files changed, 230 insertions(+), 14 deletions(-) create mode 100644 src/views/tables/TransferHistoryTable.tsx diff --git a/src/constants/abacus.ts b/src/constants/abacus.ts index 55c0ad7..551274c 100644 --- a/src/constants/abacus.ts +++ b/src/constants/abacus.ts @@ -112,6 +112,8 @@ export type SubaccountFill = Abacus.exchange.dydx.abacus.output.SubaccountFill; export type SubaccountFundingPayment = Abacus.exchange.dydx.abacus.output.SubaccountFundingPayment; export type SubaccountFundingPayments = Abacus.exchange.dydx.abacus.output.SubaccountFundingPayment[]; +export type SubaccountTransfer = Abacus.exchange.dydx.abacus.output.SubaccountTransfer; +export type SubaccountTransfers = Abacus.exchange.dydx.abacus.output.SubaccountTransfer[]; // ------ Historical PnL ------ // export type SubAccountHistoricalPNL = Abacus.exchange.dydx.abacus.output.SubaccountHistoricalPNL; diff --git a/src/constants/localization/app.ts b/src/constants/localization/app.ts index 93914c9..a392340 100644 --- a/src/constants/localization/app.ts +++ b/src/constants/localization/app.ts @@ -177,6 +177,7 @@ export const APP_STRING_KEYS = { RECEIVE: 'GENERAL.RECEIVE', RECENT: 'GENERAL.RECENT', RECENT_TRADES_SHORT: 'GENERAL.RECENT_TRADES_SHORT', + RECIPIENT: 'GENERAL.RECIPIENT', REFERRAL_CODE: 'GENERAL.REFERRAL_CODE', REFERRALS: 'GENERAL.REFERRALS', REFERRER_PERCENT_OFF: 'GENERAL.REFERRER_PERCENT_OFF', @@ -188,6 +189,7 @@ export const APP_STRING_KEYS = { SELECT_NETWORK: 'GENERAL.SELECT_NETWORK', SELL: 'GENERAL.SELL', SEND: 'GENERAL.SEND', + SENDER: 'GENERAL.SENDER', SHARE: 'GENERAL.SHARE', SHORT_POSITION_SHORT: 'GENERAL.SHORT_POSITION_SHORT', SIDE: 'GENERAL.SIDE', diff --git a/src/lib/abacus/stateNotification.ts b/src/lib/abacus/stateNotification.ts index ef08749..fbadc75 100644 --- a/src/lib/abacus/stateNotification.ts +++ b/src/lib/abacus/stateNotification.ts @@ -19,6 +19,7 @@ import { setFundingPayments, setHistoricalPnl, setSubaccount, + setTransfers, setWallet, } from '@/state/account'; @@ -102,6 +103,11 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol { dispatch(setFundingPayments(fundingPayments)); } + if (changes.has(Changes.transfers)) { + const transfers = updatedState.subaccountTransfers(subaccountId)?.toArray() || []; + dispatch(setTransfers(transfers)); + } + if (changes.has(Changes.historicalPnl)) { const historicalPnl = updatedState.subaccountHistoricalPnl(subaccountId)?.toArray() || []; diff --git a/src/localization/en/app.json b/src/localization/en/app.json index 41a0027..a492d6b 100644 --- a/src/localization/en/app.json +++ b/src/localization/en/app.json @@ -182,6 +182,7 @@ "RECEIVE": "Receive", "RECENT": "Recent", "RECENT_TRADES_SHORT": "Trades", + "RECIPIENT": "Recipient", "REFERRAL_CODE": "Referral Code", "REFERRALS": "Referrals", "REFERRER_PERCENT_OFF": "{DISCOUNT}% off", @@ -193,6 +194,7 @@ "SELECT_NETWORK": "Select Network", "SELL": "Sell", "SEND": "Send", + "SENDER": "Sender", "SHARE": "Share", "SHORT_POSITION_SHORT": "Short", "SIDE": "Side", diff --git a/src/pages/portfolio/History.tsx b/src/pages/portfolio/History.tsx index 72fffa2..78f85aa 100644 --- a/src/pages/portfolio/History.tsx +++ b/src/pages/portfolio/History.tsx @@ -28,13 +28,14 @@ export const History = () => { label:

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

, href: HistoryRoute.Trades, }, + { + value: HistoryRoute.Transfers, + label:

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

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

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

, - // href: HistoryRoute.Transfers, - // }, - // { // value: HistoryRoute.Payments, // label:

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

, // href: HistoryRoute.Payments, diff --git a/src/pages/portfolio/Portfolio.tsx b/src/pages/portfolio/Portfolio.tsx index 227af41..d8969b8 100644 --- a/src/pages/portfolio/Portfolio.tsx +++ b/src/pages/portfolio/Portfolio.tsx @@ -9,6 +9,7 @@ import { useBreakpoints, useDocumentTitle, useStringGetter } from '@/hooks'; import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable'; import { FundingPaymentsTable } from '@/views/tables/FundingPaymentsTable'; +import { TransferHistoryTable } from '@/views/tables/TransferHistoryTable'; import { Icon, IconName } from '@/components/Icon'; import { NavigationMenu } from '@/components/NavigationMenu'; import { WithSidebar } from '@/components/WithSidebar'; @@ -61,7 +62,10 @@ export default () => { /> } /> - } /> + } + /> } @@ -118,8 +122,6 @@ export default () => { }, ], }, - // TODO(aforaleka) Add back subitems when there are clearer designs - // or when transfers and payments are ready ]} /> ) diff --git a/src/pages/portfolio/PortfolioNavMobile.tsx b/src/pages/portfolio/PortfolioNavMobile.tsx index ebfaf5a..0ce1a65 100644 --- a/src/pages/portfolio/PortfolioNavMobile.tsx +++ b/src/pages/portfolio/PortfolioNavMobile.tsx @@ -40,12 +40,12 @@ export const PortfolioNavMobile = () => { label: stringGetter({ key: STRING_KEYS.TRADES }), description: stringGetter({ key: STRING_KEYS.TRADES_DESCRIPTION }), }, - // TODO: TRCL-1693 - re-enable when Payments and Transfers are ready - // { - // value: `${AppRoute.Portfolio}/${PortfolioRoute.History}/${HistoryRoute.Transfers}`, - // label: stringGetter({ key: STRING_KEYS.TRANSFERS }), - // description: stringGetter({ key: STRING_KEYS.TRANSFERS_DESCRIPTION }), - // }, + { + value: `${AppRoute.Portfolio}/${PortfolioRoute.History}/${HistoryRoute.Transfers}`, + label: stringGetter({ key: STRING_KEYS.TRANSFERS }), + description: stringGetter({ key: STRING_KEYS.TRANSFERS_DESCRIPTION }), + }, + // TODO: TRCL-1693 - re-enable when Payments are ready // { // value: `${AppRoute.Portfolio}/${PortfolioRoute.History}/${HistoryRoute.Payments}`, // label: stringGetter({ key: STRING_KEYS.PAYMENTS }), diff --git a/src/state/account.ts b/src/state/account.ts index b722363..5fab6e3 100644 --- a/src/state/account.ts +++ b/src/state/account.ts @@ -9,6 +9,7 @@ import type { SubaccountFundingPayments, Wallet, SubaccountOrder, + SubaccountTransfers, HistoricalPnlPeriods, SubAccountHistoricalPNLs, } from '@/constants/abacus'; @@ -22,6 +23,7 @@ import { getLocalStorage } from '@/lib/localStorage'; export type AccountState = { fills?: SubaccountFills; fundingPayments?: SubaccountFundingPayments; + transfers?: SubaccountTransfers; clearedOrderIds?: string[]; uncommittedOrderClientIds?: number[]; hasUnseenFillUpdates: boolean; @@ -38,6 +40,7 @@ export type AccountState = { const initialState: AccountState = { fills: undefined, fundingPayments: undefined, + transfers: undefined, clearedOrderIds: undefined, uncommittedOrderClientIds: undefined, hasUnseenFillUpdates: false, @@ -81,6 +84,9 @@ export const accountSlice = createSlice({ setFundingPayments: (state, action: PayloadAction) => { state.fundingPayments = action.payload; }, + setTransfers: (state, action: PayloadAction) => { + state.transfers = action.payload; + }, clearOrder: (state, action: PayloadAction) => ({ ...state, clearedOrderIds: [...(state.clearedOrderIds || []), action.payload], @@ -148,6 +154,7 @@ export const accountSlice = createSlice({ export const { setFills, setFundingPayments, + setTransfers, clearOrder, setOnboardingGuard, setOnboardingState, diff --git a/src/state/accountSelectors.ts b/src/state/accountSelectors.ts index c16f039..29aa72e 100644 --- a/src/state/accountSelectors.ts +++ b/src/state/accountSelectors.ts @@ -193,6 +193,12 @@ export const getCurrentMarketFills = createSelector( !currentMarketId ? [] : marketFills[currentMarketId] ); +/** + * @param state + * @returns list of transfers for the currently connected subaccount + */ +export const getSubaccountTransfers = (state: RootState) => state.account?.transfers; + /** * @param state * @returns list of funding payments for the currently connected subaccount diff --git a/src/styles/tradeViewMixins.ts b/src/styles/tradeViewMixins.ts index fa64498..a847b81 100644 --- a/src/styles/tradeViewMixins.ts +++ b/src/styles/tradeViewMixins.ts @@ -18,5 +18,9 @@ export const tradeViewMixins: Record< tbody { font: var(--font-small-book); } + + thead tr { + box-shadow: none; + } `, }; diff --git a/src/views/tables/TransferHistoryTable.tsx b/src/views/tables/TransferHistoryTable.tsx new file mode 100644 index 0000000..f5df2a9 --- /dev/null +++ b/src/views/tables/TransferHistoryTable.tsx @@ -0,0 +1,184 @@ +import styled, { type AnyStyledComponent, css } from 'styled-components'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import type { ColumnSize } from '@react-types/table'; + +import { type SubaccountTransfer } from '@/constants/abacus'; +import { STRING_KEYS, StringGetterFunction } from '@/constants/localization'; + +import { useBreakpoints, useStringGetter } from '@/hooks'; + +import { layoutMixins } from '@/styles/layoutMixins'; +import { tradeViewMixins } from '@/styles/tradeViewMixins'; + +import { Icon } from '@/components/Icon'; +import { Output, OutputType } from '@/components/Output'; +import { Table, TableCell, TableColumnHeader, type ColumnDef } from '@/components/Table'; + +import { getSubaccountTransfers } from '@/state/accountSelectors'; + +import { truncateAddress } from '@/lib/wallet'; +import { Button } from '@/components/Button'; +import { ButtonAction } from '@/constants/buttons'; +import { openDialog } from '@/state/dialogs'; +import { DialogTypes } from '@/constants/dialogs'; +import { Link } from '@/components/Link'; +import { calculateCanAccountTrade } from '@/state/accountCalculators'; +import { OnboardingTriggerButton } from '../dialogs/OnboardingTriggerButton'; + +const MOBILE_TRANSFERS_PER_PAGE = 50; + +export enum TransferHistoryTableColumnKey { + Time = 'Time', + Action = 'Action', + SenderRecipient = 'Sender-Recipient', + Amount = 'Amount', + TxHash = 'TxHash', +} + +const getTransferHistoryTableColumnDef = ({ + key, + stringGetter, + width, +}: { + key: TransferHistoryTableColumnKey; + isTablet?: boolean; + stringGetter: StringGetterFunction; + width?: ColumnSize; +}): ColumnDef => ({ + width, + ...( + { + [TransferHistoryTableColumnKey.Time]: { + columnKey: TransferHistoryTableColumnKey.Time, + getCellValue: (row) => row.updatedAtMilliseconds, + label: stringGetter({ key: STRING_KEYS.TIME }), + renderCell: ({ updatedAtMilliseconds }) => ( + + ), + }, + [TransferHistoryTableColumnKey.Action]: { + columnKey: TransferHistoryTableColumnKey.Action, + getCellValue: (row) => row.resources.typeStringKey, + label: stringGetter({ key: STRING_KEYS.ACTION }), + renderCell: ({ resources }) => + resources.typeStringKey && stringGetter({ key: resources.typeStringKey }), + }, + [TransferHistoryTableColumnKey.SenderRecipient]: { + columnKey: TransferHistoryTableColumnKey.SenderRecipient, + getCellValue: (row) => `${row.fromAddress}-${row.toAddress}`, + label: ( + + {stringGetter({ key: STRING_KEYS.SENDER })} + {stringGetter({ key: STRING_KEYS.RECIPIENT })} + + ), + renderCell: ({ fromAddress, toAddress }) => ( + + {fromAddress ? truncateAddress(fromAddress) : '-'} + {toAddress ? truncateAddress(toAddress) : '-'} + + ), + }, + [TransferHistoryTableColumnKey.Amount]: { + columnKey: TransferHistoryTableColumnKey.Amount, + getCellValue: (row) => row.amount, + label: stringGetter({ key: STRING_KEYS.AMOUNT }), + renderCell: ({ amount }) => , + }, + [TransferHistoryTableColumnKey.TxHash]: { + columnKey: TransferHistoryTableColumnKey.TxHash, + getCellValue: (row) => row.transactionHash, + label: stringGetter({ key: STRING_KEYS.TRANSACTION }), + renderCell: ({ transactionHash }) => + transactionHash ? {transactionHash} : '-', + }, + } as Record> + )[key], +}); + +type ElementProps = { + columnKeys?: TransferHistoryTableColumnKey[]; + columnWidths?: Partial>; +}; + +type StyleProps = { + withOuterBorder?: boolean; + withInnerBorders?: boolean; +}; + +export const TransferHistoryTable = ({ + columnKeys = Object.values(TransferHistoryTableColumnKey), + columnWidths, + withOuterBorder, + withInnerBorders = true, +}: ElementProps & StyleProps) => { + const stringGetter = useStringGetter(); + const dispatch = useDispatch(); + const { isMobile, isTablet } = useBreakpoints(); + + const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual); + + const transfers = useSelector(getSubaccountTransfers, shallowEqual) ?? []; + + return ( + row.id} + columns={columnKeys.map((key: TransferHistoryTableColumnKey) => + getTransferHistoryTableColumnDef({ + key, + isTablet, + stringGetter, + width: columnWidths?.[key], + }) + )} + slotEmpty={ + <> + {stringGetter({ key: STRING_KEYS.TRANSFERS_EMPTY_STATE })} + {canAccountTrade ? ( + + ) : ( + + )} + + } + selectionBehavior="replace" + withOuterBorder={withOuterBorder} + withInnerBorders={withInnerBorders} + withScrollSnapColumns + withScrollSnapRows + /> + ); +}; + +const Styled: Record = {}; + +Styled.Table = styled(Table)` + ${tradeViewMixins.horizontalTable} +`; + +Styled.InlineRow = styled.div` + ${layoutMixins.inlineRow} +`; + +Styled.Icon = styled(Icon)` + font-size: 3em; +`; + +Styled.TimeOutput = styled(Output)` + color: var(--color-text-0); +`; + +Styled.TxHash = styled(Link)` + justify-content: flex-end; +`;