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;
+`;