Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions src/constants/dialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { OrderSide } from '@/bonsai/forms/trade/types';
import { PositionUniqueId, SubaccountPosition } from '@/bonsai/types/summaryTypes';
import { TagsOf, UnionOf, ofType, unionize } from 'unionize';

import { IndexerPositionSide } from '@/types/indexer/indexerApiGen';

import { BigNumberish } from '@/lib/numbers';
import { Nullable } from '@/lib/typeUtils';

Expand Down Expand Up @@ -67,14 +65,22 @@ export type SetMarketLeverageDialogProps = { marketId: string };
export type SetupPasskeyDialogProps = { onClose: () => void };
export type ShareAffiliateDialogProps = {};
export type SharePNLAnalyticsDialogProps = {
marketId: string;
assetId: string;
leverage: Nullable<number>;
oraclePrice: Nullable<number>;
entryPrice: Nullable<number>;
unrealizedPnl: Nullable<number>;
side: Nullable<IndexerPositionSide>;
sideLabel: Nullable<string>;
marketId?: string;
isLong: boolean;
isCross: boolean;
shareType?: 'open' | 'close' | 'liquidated' | 'partialClose' | undefined;
leverage?: Nullable<number>;
size?: Nullable<number>;
prevSize?: Nullable<number>;
pnl?: Nullable<number>;
unrealizedPnl?: Nullable<number>;
pnlPercentage?: Nullable<number>;
entryPrice?: Nullable<number>;
exitPrice?: Nullable<number>;
liquidationPrice?: Nullable<number>;
oraclePrice?: Nullable<number>;
sideLabel?: Nullable<string>;
};
export type SimpleUiTradeDialogProps =
| {
Expand Down
101 changes: 34 additions & 67 deletions src/hooks/useSharePnlImage.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,42 @@
import { logBonsaiError } from '@/bonsai/logs';
import { useQuery } from '@tanstack/react-query';

import { SharePNLAnalyticsDialogProps } from '@/constants/dialogs';
import { timeUnits } from '@/constants/time';
import { IndexerPerpetualPositionStatus, IndexerPositionSide } from '@/types/indexer/indexerApiGen';

import { useAccounts } from '@/hooks/useAccounts';
import { useEndpointsConfig } from '@/hooks/useEndpointsConfig';

import { getOpenPositions } from '@/state/accountSelectors';
import { useAppSelector } from '@/state/appTypes';

import { Nullable } from '@/lib/typeUtils';
import { truncateAddress } from '@/lib/wallet';

import { useEndpointsConfig } from './useEndpointsConfig';

export type SharePnlImageParams = {
assetId: string;
marketId: string;
side: Nullable<IndexerPositionSide>;
leverage: Nullable<number>;
oraclePrice: Nullable<number>;
entryPrice: Nullable<number>;
unrealizedPnl: Nullable<number>;
type?: 'open' | 'close' | 'liquidated' | undefined;
};

export const useSharePnlImage = ({
assetId,
marketId,
side,
leverage,
oraclePrice,
entryPrice,
unrealizedPnl,
type = 'open',
}: SharePnlImageParams) => {
export const useSharePnlImage = (data: SharePNLAnalyticsDialogProps) => {
const { pnlImageApi } = useEndpointsConfig();
const { dydxAddress } = useAccounts();
const openPositions = useAppSelector(getOpenPositions);

const position = openPositions?.find((p) => p.market === marketId);

const positionType =
position?.status === IndexerPerpetualPositionStatus.CLOSED
? 'close'
: position?.status === IndexerPerpetualPositionStatus.LIQUIDATED
? 'liquidated'
: 'open';

const pnl = (position?.realizedPnl.toNumber() ?? 0) + (unrealizedPnl ?? 0);

const queryFn = async (): Promise<Blob | undefined> => {
if (!dydxAddress) {
if (!dydxAddress || !data.marketId) {
return undefined;
}

const totalPnl = (data.pnl ?? 0) + (data.unrealizedPnl ?? 0);

const requestBody = {
ticker: assetId,
type: positionType,
leverage: leverage ?? 0,
ticker: data.assetId,
type: data.shareType ?? 'open',
leverage: data.leverage ?? 0,
username: truncateAddress(dydxAddress),
isLong: side === IndexerPositionSide.LONG,
isCross: position?.marginMode === 'CROSS',
// Optional fields - include if available
size: position?.value.toNumber(),
pnl,
uPnl: unrealizedPnl ?? undefined,
pnlPercentage: position?.updatedUnrealizedPnlPercent?.toNumber(),
entryPx: entryPrice ?? undefined,
exitPx: position?.exitPrice?.toNumber(),
liquidationPx: position?.liquidationPrice?.toNumber(),
markPx: oraclePrice ?? undefined,
isLong: data.isLong,
isCross: data.isCross,
// Optional fields
size: data.size ?? undefined,
prevSize: data.prevSize ?? undefined,
pnl: totalPnl || undefined,
uPnl: data.unrealizedPnl ?? undefined,
pnlPercentage: data.pnlPercentage ?? undefined,
entryPx: data.entryPrice ?? undefined,
exitPx: data.exitPrice ?? undefined,
liquidationPx: data.liquidationPrice ?? undefined,
markPx: data.oraclePrice ?? undefined,
};

const response = await fetch(pnlImageApi, {
Expand All @@ -92,25 +58,26 @@ export const useSharePnlImage = ({
return useQuery({
queryKey: [
'sharePnlImage',
marketId,
data.marketId,
dydxAddress,
side,
leverage,
oraclePrice,
entryPrice,
unrealizedPnl,
type,
position?.marginMode,
position?.unsignedSize.toString(),
position?.liquidationPrice?.toString(),
data.isLong,
data.isCross,
data.shareType,
data.leverage,
data.size,
data.pnl,
data.unrealizedPnl,
data.entryPrice,
data.exitPrice,
data.oraclePrice,
],
queryFn,
enabled: Boolean(dydxAddress),
refetchOnWindowFocus: false,
refetchOnReconnect: false,
staleTime: 2 * timeUnits.minute, // 2 minutes
staleTime: 2 * timeUnits.minute,
retry: 2,
retryDelay: 1 * timeUnits.second, // 1 second
retryDelay: 1 * timeUnits.second,
retryOnMount: true,
});
};
1 change: 1 addition & 0 deletions src/pages/portfolio/Portfolio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ const PortfolioPage = () => {
FillsTableColumnKey.Fee,
FillsTableColumnKey.ClosedPnl,
FillsTableColumnKey.Liquidity,
FillsTableColumnKey.Actions,
]
}
withOuterBorder={isNotTablet}
Expand Down
24 changes: 5 additions & 19 deletions src/views/dialogs/SharePNLAnalyticsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,31 +42,17 @@ const copyBlobToClipboard = async (blob: Blob | null) => {
};

export const SharePNLAnalyticsDialog = ({
marketId,
assetId,
side,
leverage,
oraclePrice,
entryPrice,
unrealizedPnl,
setIsOpen,
...sharePnlData
}: DialogProps<SharePNLAnalyticsDialogProps>) => {
const stringGetter = useStringGetter();
const dispatch = useAppDispatch();
const symbol = getDisplayableAssetFromBaseAsset(assetId);
const symbol = getDisplayableAssetFromBaseAsset(sharePnlData.assetId);
const [isCopying, setIsCopying] = useState(false);
const [isSharing, setIsSharing] = useState(false);
const [isCopied, setIsCopied] = useState(false);

const getPnlImage = useSharePnlImage({
assetId,
marketId,
side,
leverage,
oraclePrice,
entryPrice,
unrealizedPnl,
});
const getPnlImage = useSharePnlImage(sharePnlData);

const pnlImage = useMemo(() => getPnlImage.data ?? undefined, [getPnlImage.data]);

Expand All @@ -75,7 +61,7 @@ export const SharePNLAnalyticsDialog = ({
setIsCopying(true);
try {
await copyBlobToClipboard(pnlImage);
track(AnalyticsEvents.SharePnlCopied({ asset: assetId }));
track(AnalyticsEvents.SharePnlCopied({ asset: sharePnlData.assetId }));
setIsCopying(false);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
Expand All @@ -100,7 +86,7 @@ export const SharePNLAnalyticsDialog = ({
})}\n\n#dydx #${symbol}\n[${stringGetter({ key: STRING_KEYS.TWEET_PASTE_IMAGE_AND_DELETE_THIS })}]`,
related: 'dYdX',
});
track(AnalyticsEvents.SharePnlShared({ asset: assetId }));
track(AnalyticsEvents.SharePnlShared({ asset: sharePnlData.assetId }));
setIsSharing(false);
} catch (error) {
logBonsaiError('SharePNLAnalyticsDialog/sharePnlImage', 'Failed to share PNL image', {
Expand Down
24 changes: 22 additions & 2 deletions src/views/tables/FillsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@
import { PageSize } from '@/components/Table/TablePaginationRow';
import { TagSize } from '@/components/Tag';

import { calculateIsAccountViewOnly } from '@/state/accountCalculators';
import { useAppDispatch, useAppSelector } from '@/state/appTypes';
import { openDialog } from '@/state/dialogs';

import { mapIfPresent } from '@/lib/do';
import { MustBigNumber } from '@/lib/numbers';
import { MaybeBigNumber, MustBigNumber } from '@/lib/numbers';
import { getHydratedFill } from '@/lib/orders';
import { Nullable, orEmptyRecord } from '@/lib/typeUtils';

Expand All @@ -39,6 +40,7 @@
getIndexerLiquidityStringKey,
getIndexerOrderSideStringKey,
} from '../../lib/enumToStringKeyHelpers';
import { FillActionsCell } from './FillsTable/FillActionsCell';

Check failure on line 43 in src/views/tables/FillsTable.tsx

View workflow job for this annotation

GitHub Actions / lint

Dependency cycle detected

export enum FillsTableColumnKey {
Time = 'Time',
Expand All @@ -53,6 +55,7 @@
Total = 'Total',
Fee = 'Fee',
ClosedPnl = 'ClosedPnl',
Actions = 'Actions',

// Tablet Only
TypeAmount = 'Type-Amount',
Expand All @@ -70,11 +73,13 @@
stringGetter,
symbol = '',
width,
isAccountViewOnly,
}: {
key: FillsTableColumnKey;
stringGetter: StringGetterFunction;
symbol?: Nullable<string>;
width?: ColumnSize;
isAccountViewOnly?: boolean;
}): ColumnDef<FillTableRow> => ({
width,
...(
Expand Down Expand Up @@ -258,7 +263,6 @@
label: stringGetter({ key: STRING_KEYS.SIDE }),
renderCell: ({ side }) => side && <OrderSideTag orderSide={side} size={TagSize.Medium} />,
},

[FillsTableColumnKey.AmountPrice]: {
columnKey: 'sizePrice',
getCellValue: (row) => row.size,
Expand All @@ -280,6 +284,20 @@
</TableCell>
),
},
[FillsTableColumnKey.Actions]: {
columnKey: 'actions',
label: '',
isActionable: true,
allowsSorting: false,
renderCell: ({ marketSummary, ...fill }: FillTableRow) => (
<FillActionsCell
fill={fill}
assetId={marketSummary?.assetId ?? ''}
oraclePrice={MaybeBigNumber(marketSummary?.oraclePrice)?.toNumber()}
isDisabled={isAccountViewOnly}
/>
),
},
} satisfies Record<FillsTableColumnKey, ColumnDef<FillTableRow>>
)[key],
});
Expand Down Expand Up @@ -316,6 +334,7 @@
const fills = currentMarket ? marketFills : allFills;

const marketSummaries = orEmptyRecord(useAppSelector(BonsaiCore.markets.markets.data));
const isAccountViewOnly = useAppSelector(calculateIsAccountViewOnly);

useViewPanel(currentMarket, 'fills');

Expand Down Expand Up @@ -352,6 +371,7 @@
stringGetter,
symbol,
width: columnWidths?.[key],
isAccountViewOnly,
})
)}
slotEmpty={
Expand Down
Loading
Loading