diff --git a/src/App.tsx b/src/App.tsx
index 26d695c..7040e37 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -106,13 +106,13 @@ const App = () => (
-
-
-
+
+
+
-
-
-
+
+
+
diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx
index 4d84511..7aba924 100644
--- a/src/components/Toast.tsx
+++ b/src/components/Toast.tsx
@@ -17,6 +17,7 @@ type ElementProps = {
slotIcon?: React.ReactNode;
slotTitle?: React.ReactNode;
slotDescription?: React.ReactNode;
+ slotCustomContent?: React.ReactNode;
slotAction?: React.ReactNode;
actionDescription?: string;
actionAltText?: string;
@@ -33,6 +34,7 @@ export const Toast = ({
slotIcon,
slotTitle,
slotDescription,
+ slotCustomContent,
slotAction,
actionDescription = '',
actionAltText = actionDescription,
@@ -78,8 +80,8 @@ export const Toast = ({
<$Title>{slotTitle}$Title>
$Header>
-
- <$Description>{slotDescription}$Description>
+ {!slotCustomContent && <$Description>{slotDescription}$Description>}
+ {slotCustomContent}
{actionDescription && (
<$Action asChild altText={actionAltText}>
{slotAction}
diff --git a/src/constants/localStorage.ts b/src/constants/localStorage.ts
index 919900e..03f9f80 100644
--- a/src/constants/localStorage.ts
+++ b/src/constants/localStorage.ts
@@ -12,6 +12,7 @@ export enum LocalStorageKey {
NotificationsLastUpdated = 'dydx.NotificationsLastUpdated',
PushNotificationsEnabled = 'dydx.PushNotificationsEnabled',
PushNotificationsLastUpdated = 'dydx.PushNotificationsLastUpdated',
+ TransferNotifications = 'dydx.TransferNotifications',
// UI State
LastViewedMarket = 'dydx.LastViewedMarket',
diff --git a/src/constants/localization/app.ts b/src/constants/localization/app.ts
index 0942a2b..573a1c5 100644
--- a/src/constants/localization/app.ts
+++ b/src/constants/localization/app.ts
@@ -902,6 +902,7 @@ export const APP_STRING_KEYS = {
'66_REDUCTION': 'NOTIFICATIONS.66_REDUCTION',
ADJUSTED_IMR: 'NOTIFICATIONS.ADJUSTED_IMR',
ADJUSTED_IMR_BODY: 'NOTIFICATIONS.ADJUSTED_IMR_BODY',
+ BRIDGING_TOKENS: "NOTIFICATIONS.BRIDGING_TOKENS",
CLOSE_MARKET_POSITIONS: 'NOTIFICATIONS.CLOSE_MARKET_POSITIONS',
CLOSE_MARKET_POSITIONS_BODY: 'NOTIFICATIONS.CLOSE_MARKET_POSITIONS_BODY',
COMPLIANCE_ALERT: 'NOTIFICATIONS.COMPLIANCE_ALERT',
@@ -909,6 +910,7 @@ export const APP_STRING_KEYS = {
DEPOSIT_IN_PROGRESS: 'NOTIFICATIONS.DEPOSIT_IN_PROGRESS',
DEPOSIT_SUCCESS_DESCRIPTION: 'NOTIFICATIONS.DEPOSIT_SUCCESS_DESCRIPTION',
DEPOSIT_SUCCESS: 'NOTIFICATIONS.DEPOSIT_SUCCESS',
+ DEPOSIT_TO_CHAIN: "NOTIFICATIONS.DEPOSIT_TO_CHAIN",
DIRECT_TRANSFER_SUCCESS: 'NOTIFICATIONS.DIRECT_TRANSFER_SUCCESS',
DIRECT_TRANSFER_SUCCESS_DESCRIPTION: 'NOTIFICATIONS.DIRECT_TRANSFER_SUCCESS_DESCRIPTION',
EPOCH_REWARDS: 'NOTIFICATIONS.EPOCH_REWARDS',
@@ -917,6 +919,8 @@ export const APP_STRING_KEYS = {
FAST_WITHDRAW_PENDING: 'NOTIFICATIONS.FAST_WITHDRAW_PENDING',
FAST_WITHDRAW_SUCCESS_DESCRIPTION: 'NOTIFICATIONS.FAST_WITHDRAW_SUCCESS_DESCRIPTION',
FAST_WITHDRAW_SUCCESS: 'NOTIFICATIONS.FAST_WITHDRAW_SUCCESS',
+ INITIATED_DEPOSIT: "NOTIFICATIONS.INITIATED_DEPOSIT",
+ INITIATED_WITHDRAWAL: "NOTIFICATIONS.INITIATED_WITHDRAWAL",
LEGAL_UPDATES: 'NOTIFICATIONS.LEGAL_UPDATES',
LEGAL_UPDATES_DESCRIPTION: 'NOTIFICATIONS.LEGAL_UPDATES_DESCRIPTION',
MAINTENANCE_MARGIN_ADJUSTMENTS: 'NOTIFICATIONS.MAINTENANCE_MARGIN_ADJUSTMENTS',
@@ -934,6 +938,7 @@ export const APP_STRING_KEYS = {
SUSPICIOUS_TRADE: 'NOTIFICATIONS.SUSPICIOUS_TRADE',
SUSPICIOUS_TRADE_BODY: 'NOTIFICATIONS.SUSPICIOUS_TRADE_BODY',
WITHDRAW_IN_PROGRESS: 'NOTIFICATIONS.WITHDRAW_IN_PROGRESS',
+ WITHDRAW_TO_CHAIN: "NOTIFICATIONS.WITHDRAW_TO_CHAIN",
// Email Notifications
ACCOUNT_DESCRIPTION: 'EMAIL_NOTIFICATIONS.ACCOUNT_DESCRIPTION',
diff --git a/src/constants/notifications.ts b/src/constants/notifications.ts
index bde7409..698158d 100644
--- a/src/constants/notifications.ts
+++ b/src/constants/notifications.ts
@@ -1,3 +1,5 @@
+import { StatusResponse } from "@0xsquid/sdk";
+
/** implemented in useNotificationTypes */
export enum NotificationType {
OrderStatusChanged = 'OrderStatusChanged',
@@ -83,6 +85,12 @@ export type NotificationDisplayData = {
description?: React.ReactNode;
+ customContent?: React.ReactNode;
+
+ customMenuContent?: React.ReactNode;
+
+ customMenuSlotAfter?: React.ReactNode;
+
actionDescription?: string;
/** Screen reader: instructions for performing toast action after its timer expires */
@@ -112,8 +120,9 @@ export type NotificationDisplayData = {
// Notification types
export type TransferNotifcation = {
txHash: string;
- toChainId: string;
+ toChainId?: string;
fromChainId?: string;
toAmount?: number;
triggeredAt?: number;
+ status?: StatusResponse;
};
diff --git a/src/hooks/useNotificationTypes.tsx b/src/hooks/useNotificationTypes.tsx
index ba662e8..cea23f5 100644
--- a/src/hooks/useNotificationTypes.tsx
+++ b/src/hooks/useNotificationTypes.tsx
@@ -1,15 +1,19 @@
-import { useEffect, useMemo } from 'react';
+import { useCallback, useEffect, useMemo } from 'react';
import { useSelector, shallowEqual, useDispatch } from 'react-redux';
import { groupBy } from 'lodash';
import { AbacusOrderStatus, ORDER_SIDES, ORDER_STATUS_STRINGS } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
-import { type NotificationTypeConfig, type TransferNotifcation, NotificationType } from '@/constants/notifications';
+import {
+ type NotificationTypeConfig,
+ type TransferNotifcation,
+ NotificationType,
+} from '@/constants/notifications';
import { ORDER_SIDE_STRINGS, TRADE_TYPE_STRINGS, TradeTypes } from '@/constants/trade';
import { Icon, IconName } from '@/components/Icon';
-import { TransferStatusToast } from '@/components/TransferStatus';
+import { TransferStatusToast } from '@/views/TransferStatus';
import {
getSquidTransfers,
@@ -21,113 +25,126 @@ import { openDialog } from '@/state/dialogs';
import { OrderStatusIcon } from '@/views/OrderStatusIcon';
import { useStringGetter } from './useStringGetter';
+import { TransferStatusSteps } from '@/views/TransferStatusSteps';
+import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client';
-export const notificationTypes = (
- transferNotifications: TransferNotifcation[]
-) => [
- {
- type: NotificationType.OrderStatusChanged,
+export const notificationTypes = (transferNotifications: TransferNotifcation[]) =>
+ [
+ {
+ type: NotificationType.OrderStatusChanged,
- useTrigger: ({ trigger, lastUpdated }) => {
- const stringGetter = useStringGetter();
+ useTrigger: ({ trigger, lastUpdated }) => {
+ const stringGetter = useStringGetter();
- const orders = useSelector(getSubaccountOrders, shallowEqual) || [];
- const ordersByOrderId = Object.fromEntries(orders.map((order) => [order.id, order]));
+ const orders = useSelector(getSubaccountOrders, shallowEqual) || [];
+ const ordersByOrderId = Object.fromEntries(orders.map((order) => [order.id, order]));
- const fills = useSelector(getSubaccountFills, shallowEqual) || [];
- const fillsByOrderId = groupBy(fills, (fill) => fill.orderId);
+ const fills = useSelector(getSubaccountFills, shallowEqual) || [];
+ const fillsByOrderId = groupBy(fills, (fill) => fill.orderId);
- const orderIds = useMemo(
- () => [...Object.keys(ordersByOrderId), ...Object.keys(fillsByOrderId)],
- [orders, fills]
- );
-
- useEffect(() => {
- for (const orderId of orderIds) {
- const fills = fillsByOrderId[orderId];
-
- const order =
- ordersByOrderId[orderId] ??
- (fills?.length
- ? {
- ...fills[fills.length - 1],
- id: orderId,
- createdAtMilliseconds: Math.max(
- ...fills.map((fill) => fill.createdAtMilliseconds)
- ),
- status: AbacusOrderStatus.filled,
- }
- : undefined);
-
- if (order)
- trigger(
- order.id,
- {
- icon: (
-
- ),
- title: `${stringGetter({
- key: TRADE_TYPE_STRINGS[order.type.rawValue as TradeTypes].tradeTypeKey,
- })} ${
- order.status === AbacusOrderStatus.open && order.totalFilled > 0
- ? stringGetter({ key: STRING_KEYS.PARTIALLY_FILLED })
- : stringGetter({ key: ORDER_STATUS_STRINGS[order.status.name] })
- }`,
- description: `${stringGetter({
- key: ORDER_SIDE_STRINGS[ORDER_SIDES[order.side.name]],
- })} ${order.size} ${order.marketId} @ $${order.price}`,
- actionDescription: 'View Order',
- actionAltText: 'View this order in the Orders tab or the Notifications menu.',
- toastSensitivity:
- order.status === AbacusOrderStatus.pending ? 'foreground' : 'background',
- toastDuration: 5000,
- },
- [order.status.name, order.size],
- !order.createdAtMilliseconds || order.createdAtMilliseconds > lastUpdated
- );
- }
- }, [orderIds]);
- },
-
- useNotificationAction: () => {
- const dispatch = useDispatch();
-
- return (orderId) => {
- dispatch(
- openDialog({
- type: DialogTypes.OrderDetails,
- dialogProps: { orderId },
- })
+ const orderIds = useMemo(
+ () => [...Object.keys(ordersByOrderId), ...Object.keys(fillsByOrderId)],
+ [orders, fills]
);
- };
- },
- } as NotificationTypeConfig,
- {
- type: NotificationType.SquidTransfer,
- useTrigger: ({ trigger, lastUpdated }) => {
- useEffect(() => {
- for (const transfer of transferNotifications) {
- const transferHash = transfer.txHash;
- trigger(
- transferHash,
- {
- icon: ,
- title: `Deposit in progress...`,
- description: (
-
- ),
- toastSensitivity: 'foreground',
- },
- [],
+
+ useEffect(() => {
+ for (const orderId of orderIds) {
+ const fills = fillsByOrderId[orderId];
+
+ const order =
+ ordersByOrderId[orderId] ??
+ (fills?.length
+ ? {
+ ...fills[fills.length - 1],
+ id: orderId,
+ createdAtMilliseconds: Math.max(
+ ...fills.map((fill) => fill.createdAtMilliseconds)
+ ),
+ status: AbacusOrderStatus.filled,
+ }
+ : undefined);
+
+ if (order)
+ trigger(
+ order.id,
+ {
+ icon: (
+
+ ),
+ title: `${stringGetter({
+ key: TRADE_TYPE_STRINGS[order.type.rawValue as TradeTypes].tradeTypeKey,
+ })} ${
+ order.status === AbacusOrderStatus.open && order.totalFilled > 0
+ ? stringGetter({ key: STRING_KEYS.PARTIALLY_FILLED })
+ : stringGetter({ key: ORDER_STATUS_STRINGS[order.status.name] })
+ }`,
+ description: `${stringGetter({
+ key: ORDER_SIDE_STRINGS[ORDER_SIDES[order.side.name]],
+ })} ${order.size} ${order.marketId} @ $${order.price}`,
+ actionDescription: 'View Order',
+ actionAltText: 'View this order in the Orders tab or the Notifications menu.',
+ toastSensitivity:
+ order.status === AbacusOrderStatus.pending ? 'foreground' : 'background',
+ toastDuration: 5000,
+ },
+ [order.status.name, order.size],
+ !order.createdAtMilliseconds || order.createdAtMilliseconds > lastUpdated
+ );
+ }
+ }, [orderIds]);
+ },
+
+ useNotificationAction: () => {
+ const dispatch = useDispatch();
+
+ return (orderId) => {
+ dispatch(
+ openDialog({
+ type: DialogTypes.OrderDetails,
+ dialogProps: { orderId },
+ })
);
- }
- }, [transferNotifications]);
+ };
+ },
+ } as NotificationTypeConfig,
+ {
+ type: NotificationType.SquidTransfer,
+ useTrigger: ({ trigger, lastUpdated }) => {
+ const stringGetter = useStringGetter();
+
+ const getTitleStringKey = useCallback((type: 'deposit' | 'withdraw', finished: boolean) => {
+ if (type === 'deposit' && !finished) return STRING_KEYS.DEPOSIT_IN_PROGRESS;
+ if (type === 'deposit' && finished) return STRING_KEYS.DEPOSIT;
+ if (type === 'withdraw' && !finished) return STRING_KEYS.WITHDRAW_IN_PROGRESS;
+ return STRING_KEYS.WITHDRAW;
+ }, []);
+
+ useEffect(() => {
+ for (const transfer of transferNotifications) {
+ const { toChainId, status, txHash, toAmount } = transfer;
+ const finished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
+ const type = toChainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdraw';
+
+ trigger(
+ txHash,
+ {
+ icon: ,
+ title: stringGetter({ key: getTitleStringKey(type, finished) }),
+ description: type === 'deposit' ? `Deposit of $${toAmount}` : `Withdraw of ${toAmount}`,
+ customContent: (
+
+ ),
+ customMenuContent: !finished && ,
+ toastSensitivity: 'foreground',
+ },
+ []
+ );
+ }
+ }, [transferNotifications]);
+ },
},
- },
-] satisfies NotificationTypeConfig[];
+ ] satisfies NotificationTypeConfig[];
diff --git a/src/hooks/useNotifications.tsx b/src/hooks/useNotifications.tsx
index 77c755b..c4677f6 100644
--- a/src/hooks/useNotifications.tsx
+++ b/src/hooks/useNotifications.tsx
@@ -1,4 +1,5 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
+import { useQuery } from 'react-query';
import { LocalStorageKey } from '@/constants/localStorage';
import {
@@ -9,11 +10,12 @@ import {
NotificationStatus,
} from '@/constants/notifications';
+import { useSquid } from '@/hooks/useSquid';
import { useLocalStorage } from './useLocalStorage';
import { notificationTypes as notificationTypesFactory } from './useNotificationTypes';
import { renderSvgToDataUrl } from '../lib/renderSvgToDataUrl';
-import { set } from 'lodash';
+import { StatusResponse } from '@0xsquid/sdk';
const NotificationsContext = createContext | undefined>(
undefined
@@ -39,8 +41,11 @@ const useNotificationsContext = () => {
defaultValue: Date.now(),
});
- // Front-end only notifications
- const [transferNotifications, setTransferNotifications] = useState([]);
+ // transfer notifications
+ const [transferNotifications, setTransferNotifications] = useLocalStorage({
+ key: LocalStorageKey.TransferNotifications,
+ defaultValue: [],
+ });
const addTransferNotification = useCallback(
(notification: TransferNotifcation) =>
@@ -53,6 +58,41 @@ const useNotificationsContext = () => {
[transferNotifications]
);
+ const squid = useSquid();
+
+ const { data: transferStatuses } = useQuery({
+ queryKey: ['getTransactionStatus', transferNotifications],
+ queryFn: async () => {
+ const statuses: { [key: string]: StatusResponse } = {};
+ for (const {
+ txHash,
+ toChainId,
+ fromChainId,
+ status: currentStatus,
+ } of transferNotifications) {
+ if (currentStatus && currentStatus?.squidTransactionStatus !== 'ongoing') continue;
+
+ const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId });
+ if (status) statuses[txHash] = status;
+ }
+ return statuses;
+ },
+ refetchInterval: 10_000,
+ });
+
+ useEffect(() => {
+ if (!transferStatuses) return;
+ const newTransferNotifications = transferNotifications.map((notification) => {
+ const status = transferStatuses[notification.txHash];
+ if (!status) return notification;
+ return {
+ ...notification,
+ status,
+ };
+ });
+ setTransferNotifications(newTransferNotifications);
+ }, [transferStatuses]);
+
useEffect(() => {
setNotificationsLastUpdated(Date.now());
}, [notifications]);
diff --git a/src/layout/NotificationsToastArea.tsx b/src/layout/NotificationsToastArea.tsx
index 69e7e61..d86ab60 100644
--- a/src/layout/NotificationsToastArea.tsx
+++ b/src/layout/NotificationsToastArea.tsx
@@ -41,6 +41,7 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
slotIcon={displayData.icon}
slotTitle={displayData.title}
slotDescription={displayData.description}
+ slotCustomContent={displayData.customContent}
slotAction={