Deposit/Withdraw Status Notifications
This commit is contained in:
+6
-6
@@ -106,13 +106,13 @@ const App = () => (
|
||||
<DydxProvider>
|
||||
<AccountsProvider>
|
||||
<SubaccountProvider>
|
||||
<NotificationsProvider>
|
||||
<DialogAreaProvider>
|
||||
<SquidProvider>
|
||||
<SquidProvider>
|
||||
<NotificationsProvider>
|
||||
<DialogAreaProvider>
|
||||
<Content />
|
||||
</SquidProvider>
|
||||
</DialogAreaProvider>
|
||||
</NotificationsProvider>
|
||||
</DialogAreaProvider>
|
||||
</NotificationsProvider>
|
||||
</SquidProvider>
|
||||
</SubaccountProvider>
|
||||
</AccountsProvider>
|
||||
</DydxProvider>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -12,6 +12,7 @@ export enum LocalStorageKey {
|
||||
NotificationsLastUpdated = 'dydx.NotificationsLastUpdated',
|
||||
PushNotificationsEnabled = 'dydx.PushNotificationsEnabled',
|
||||
PushNotificationsLastUpdated = 'dydx.PushNotificationsLastUpdated',
|
||||
TransferNotifications = 'dydx.TransferNotifications',
|
||||
|
||||
// UI State
|
||||
LastViewedMarket = 'dydx.LastViewedMarket',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
+122
-105
@@ -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: (
|
||||
<OrderStatusIcon status={order.status} totalFilled={order.totalFilled ?? 0} />
|
||||
),
|
||||
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<string, [string, number]>,
|
||||
{
|
||||
type: NotificationType.SquidTransfer,
|
||||
useTrigger: ({ trigger, lastUpdated }) => {
|
||||
useEffect(() => {
|
||||
for (const transfer of transferNotifications) {
|
||||
const transferHash = transfer.txHash;
|
||||
trigger(
|
||||
transferHash,
|
||||
{
|
||||
icon: <Icon iconName={IconName.Clock} />,
|
||||
title: `Deposit in progress...`,
|
||||
description: (
|
||||
<TransferStatusToast
|
||||
txHash={transferHash}
|
||||
toChainId={transfer.toChainId}
|
||||
fromChainId={transfer.fromChainId}
|
||||
toAmount={transfer.toAmount}
|
||||
triggeredAt={transfer.triggeredAt}
|
||||
/>
|
||||
),
|
||||
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: (
|
||||
<OrderStatusIcon status={order.status} totalFilled={order.totalFilled ?? 0} />
|
||||
),
|
||||
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<string, [string, number]>,
|
||||
{
|
||||
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: <Icon iconName={finished ? IconName.Transfer : IconName.Clock} />,
|
||||
title: stringGetter({ key: getTitleStringKey(type, finished) }),
|
||||
description: type === 'deposit' ? `Deposit of $${toAmount}` : `Withdraw of ${toAmount}`,
|
||||
customContent: (
|
||||
<TransferStatusToast
|
||||
toAmount={transfer.toAmount}
|
||||
triggeredAt={transfer.triggeredAt}
|
||||
status={transfer.status}
|
||||
/>
|
||||
),
|
||||
customMenuContent: !finished && <TransferStatusSteps status={transfer.status} />,
|
||||
toastSensitivity: 'foreground',
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
}, [transferNotifications]);
|
||||
},
|
||||
},
|
||||
},
|
||||
] satisfies NotificationTypeConfig[];
|
||||
] satisfies NotificationTypeConfig[];
|
||||
|
||||
@@ -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<ReturnType<typeof useNotificationsContext> | undefined>(
|
||||
undefined
|
||||
@@ -39,8 +41,11 @@ const useNotificationsContext = () => {
|
||||
defaultValue: Date.now(),
|
||||
});
|
||||
|
||||
// Front-end only notifications
|
||||
const [transferNotifications, setTransferNotifications] = useState<TransferNotifcation[]>([]);
|
||||
// transfer notifications
|
||||
const [transferNotifications, setTransferNotifications] = useLocalStorage<TransferNotifcation[]>({
|
||||
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]);
|
||||
|
||||
@@ -41,6 +41,7 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
|
||||
slotIcon={displayData.icon}
|
||||
slotTitle={displayData.title}
|
||||
slotDescription={displayData.description}
|
||||
slotCustomContent={displayData.customContent}
|
||||
slotAction={
|
||||
<Button size={ButtonSize.Small} onClick={() => onNotificationAction(notification)}>
|
||||
{displayData.actionDescription}
|
||||
|
||||
@@ -917,6 +917,7 @@
|
||||
"66_REDUCTION": "66% reduction",
|
||||
"ADJUSTED_IMR": "Adjusted Margin",
|
||||
"ADJUSTED_IMR_BODY": "We have temporarily {ACTION} the Initial Margin Requirements on {MARKET} to {PERCENT} due to volatility. {ADDITIONAL_NOTE}",
|
||||
"BRIDGING_TOKENS": "Bridging tokens",
|
||||
"CLOSE_MARKET_POSITIONS": "Close your {MARKET} position",
|
||||
"CLOSE_MARKET_POSITIONS_BODY": "{MARKET} is set to close only mode and the close price of {MARKET} is now fixed. Please close your open {MARKET} position as soon as you are able. We're here to help via the help chat if you run into issues.",
|
||||
"COMPLIANCE_ALERT": "Compliance alert",
|
||||
@@ -924,6 +925,7 @@
|
||||
"DEPOSIT_IN_PROGRESS": "Deposit in progress",
|
||||
"DEPOSIT_SUCCESS_DESCRIPTION": "Your deposit of {AMOUNT_ELEMENT} has been confirmed and is now available for trading.",
|
||||
"DEPOSIT_SUCCESS": "Deposit success!",
|
||||
"DEPOSIT_TO_CHAIN": "Deposit to {CHAIN}",
|
||||
"DIRECT_TRANSFER_SUCCESS": "Direct transfer success!",
|
||||
"DIRECT_TRANSFER_SUCCESS_DESCRIPTION": "Your direct transfer of {AMOUNT_ELEMENT} has been confirmed.",
|
||||
"EPOCH_REWARDS": "Epoch {EPOCH_NUMBER} Rewards",
|
||||
@@ -932,6 +934,8 @@
|
||||
"FAST_WITHDRAW_PENDING": "Fast withdraw pending",
|
||||
"FAST_WITHDRAW_SUCCESS_DESCRIPTION": "Your fast withdraw of {AMOUNT_ELEMENT} has been confirmed.",
|
||||
"FAST_WITHDRAW_SUCCESS": "Fast withdraw success!",
|
||||
"INITIATED_DEPOSIT": "Initiated deposit",
|
||||
"INITIATED_WITHDRAWAL": "Initiated withdrawal",
|
||||
"LEGAL_UPDATES": "Legal updates",
|
||||
"LEGAL_UPDATES_DESCRIPTION": "Please check out our updated {TOU} and {PRIVACY_POLICY}.",
|
||||
"MAINTENANCE_MARGIN_ADJUSTMENTS": "Maintenance margin requirements {ACTION} at {PERCENT}%.",
|
||||
@@ -948,7 +952,8 @@
|
||||
"SLOW_WITHDRAW_PENDING": "Slow withdraw(s) pending",
|
||||
"SUSPICIOUS_TRADE": "Suspicious activity",
|
||||
"SUSPICIOUS_TRADE_BODY": "We have noticed suspicious trading activity related to your account on dYdX, including potential wash trading activity. We will be reviewing your activity on an ongoing basis. If we notice similar activity in the future, then you will be permanently blocked from performing transfers within the protocol and from placing orders other than market orders that reduce your positions.",
|
||||
"WITHDRAW_IN_PROGRESS": "Withdrawal(s) in progress"
|
||||
"WITHDRAW_IN_PROGRESS": "Withdrawal(s) in progress",
|
||||
"WITHDRAW_TO_CHAIN": "Withdraw to {CHAIN}"
|
||||
},
|
||||
"EMAIL_NOTIFICATIONS": {
|
||||
"ACCOUNT_DESCRIPTION": "Deposits, Withdrawals, Account Updates",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import styled, { css, keyframes, type AnyStyledComponent } from 'styled-components';
|
||||
import { Root, Trigger, Content } from '@radix-ui/react-collapsible';
|
||||
import { StatusResponse } from '@0xsquid/sdk';
|
||||
|
||||
import type { TransferStatus } from '@/constants/abacus';
|
||||
|
||||
@@ -17,33 +18,28 @@ import { Output, OutputType } from '@/components/Output';
|
||||
import { Link } from '@/components/Link';
|
||||
import { WithReceipt } from '@/components/WithReceipt';
|
||||
import { Icon, IconName } from '@/components/Icon';
|
||||
import { TransferStatusSteps } from '@/views/TransferStatusSteps';
|
||||
import { LoadingDots } from '@/components/Loading/LoadingDots';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
import { popoverMixins } from '@/styles/popoverMixins';
|
||||
import { DateTime } from 'luxon';
|
||||
import { LoadingDots } from './Loading/LoadingDots';
|
||||
|
||||
type ElementProps = {
|
||||
txHash: string;
|
||||
toChainId: string;
|
||||
fromChainId?: string;
|
||||
toAmount?: number;
|
||||
triggeredAt?: number;
|
||||
status?: StatusResponse;
|
||||
};
|
||||
|
||||
export const TransferStatusToast = ({ txHash, toChainId, fromChainId, toAmount, triggeredAt = Date.now() }: ElementProps) => {
|
||||
export const TransferStatusToast = ({
|
||||
toAmount,
|
||||
triggeredAt = Date.now(),
|
||||
status,
|
||||
}: ElementProps) => {
|
||||
const stringGetter = useStringGetter();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [secondsLeft, setSecondsLeft] = useState<number | undefined>();
|
||||
|
||||
const squid = useSquid();
|
||||
|
||||
const { data: status } = useQuery({
|
||||
queryKey: ['getTransactionStatus', { transactionId: txHash, toChainId, fromChainId }],
|
||||
queryFn: async () => await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId }),
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
|
||||
const updateSecondsLeft = useCallback(() => {
|
||||
const fromChainEta = (status?.fromChain?.chainData?.estimatedRouteDuration || 0) * 1000;
|
||||
const toChainEta = (status?.toChain?.chainData?.estimatedRouteDuration || 0) * 1000;
|
||||
@@ -60,22 +56,9 @@ export const TransferStatusToast = ({ txHash, toChainId, fromChainId, toAmount,
|
||||
hideReceipt={!open}
|
||||
side="bottom"
|
||||
slotReceipt={
|
||||
<Styled.Content>
|
||||
{/* {statusSteps.map(({ status, label, url, eta }, index) => {
|
||||
return (
|
||||
<Styled.Step key={index}>
|
||||
{url ? (
|
||||
<Link href={url}>
|
||||
<span>{label}</span>
|
||||
<Icon iconName={IconName.LinkOut} />
|
||||
</Link>
|
||||
) : (
|
||||
<span>{label}</span>
|
||||
)}
|
||||
</Styled.Step>
|
||||
);
|
||||
})} */}
|
||||
</Styled.Content>
|
||||
<Styled.Receipt>
|
||||
<TransferStatusSteps status={status} />
|
||||
</Styled.Receipt>
|
||||
}
|
||||
>
|
||||
<Styled.BridgingStatus>
|
||||
@@ -83,16 +66,11 @@ export const TransferStatusToast = ({ txHash, toChainId, fromChainId, toAmount,
|
||||
{stringGetter({
|
||||
key: STRING_KEYS.DEPOSIT_STATUS,
|
||||
params: {
|
||||
AMOUNT_USD: (
|
||||
<Styled.InlineOutput
|
||||
type={OutputType.Fiat}
|
||||
value={toAmount}
|
||||
/>
|
||||
),
|
||||
AMOUNT_USD: <Styled.InlineOutput type={OutputType.Fiat} value={toAmount} />,
|
||||
ESTIMATED_DURATION: (
|
||||
<Styled.InlineOutput
|
||||
type={OutputType.Text}
|
||||
value={formatSeconds(secondsLeft || 0)}
|
||||
value={formatSeconds(Math.max(secondsLeft || 0, 0))}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -113,7 +91,13 @@ export const TransferStatusToast = ({ txHash, toChainId, fromChainId, toAmount,
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.Root = styled(Root)`
|
||||
margin: 0 -1rem -1rem -1rem;
|
||||
margin: 0.5rem -1rem -1rem -1rem;
|
||||
color: var(--color-text-0);
|
||||
font: var(--font-small-book);
|
||||
`;
|
||||
|
||||
Styled.Receipt = styled.div`
|
||||
padding: 0 1rem;
|
||||
`;
|
||||
|
||||
Styled.BridgingStatus = styled.div`
|
||||
@@ -0,0 +1,178 @@
|
||||
import { useMemo } from 'react';
|
||||
import styled, { css, keyframes, type AnyStyledComponent } from 'styled-components';
|
||||
import { StatusResponse } from '@0xsquid/sdk';
|
||||
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client';
|
||||
|
||||
import { useStringGetter } from '@/hooks';
|
||||
|
||||
import { Link } from '@/components/Link';
|
||||
import { Icon, IconName } from '@/components/Icon';
|
||||
import { LoadingDots } from '@/components/Loading/LoadingDots';
|
||||
import { LoadingSpinner } from '@/components/Loading/LoadingSpinner';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
|
||||
type ElementProps = {
|
||||
status?: StatusResponse;
|
||||
};
|
||||
|
||||
enum TransferStatusStep {
|
||||
FromChain,
|
||||
Bridge,
|
||||
ToChain,
|
||||
Complete,
|
||||
}
|
||||
|
||||
export const TransferStatusSteps = ({ status }: ElementProps) => {
|
||||
const stringGetter = useStringGetter();
|
||||
|
||||
const type = useMemo(
|
||||
() => (status?.toChain?.chainData.chainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdrawal'),
|
||||
[status]
|
||||
);
|
||||
|
||||
const currentStep = useMemo(() => {
|
||||
const routeStatus = status?.routeStatus;
|
||||
const fromChain = status?.fromChain?.chainData.chainId;
|
||||
const toChain = status?.toChain?.chainData.chainId;
|
||||
|
||||
if (!routeStatus?.length) return TransferStatusStep.FromChain;
|
||||
|
||||
const currentStatus = routeStatus[routeStatus?.length - 1];
|
||||
|
||||
if (currentStatus.chainId === toChain) {
|
||||
return currentStatus.status !== 'success'
|
||||
? TransferStatusStep.ToChain
|
||||
: TransferStatusStep.Complete;
|
||||
}
|
||||
|
||||
if (currentStatus.chainId === fromChain && currentStatus.status !== 'success') {
|
||||
return TransferStatusStep.FromChain;
|
||||
}
|
||||
|
||||
return TransferStatusStep.Bridge;
|
||||
}, [status]);
|
||||
|
||||
const steps = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: stringGetter({
|
||||
key: type === 'deposit' ? STRING_KEYS.INITIATED_DEPOSIT : STRING_KEYS.INITIATED_WITHDRAWAL,
|
||||
}),
|
||||
step: TransferStatusStep.FromChain,
|
||||
link: status?.fromChain?.transactionUrl,
|
||||
},
|
||||
{
|
||||
label: 'Bridging tokens',
|
||||
step: TransferStatusStep.Bridge,
|
||||
link: status?.axelarTransactionUrl,
|
||||
},
|
||||
{
|
||||
label: stringGetter({
|
||||
key: type === 'deposit' ? STRING_KEYS.DEPOSIT_TO_CHAIN : STRING_KEYS.WITHDRAW_TO_CHAIN,
|
||||
params: {
|
||||
CHAIN: status?.toChain?.chainData.chainName,
|
||||
}
|
||||
}),
|
||||
step: TransferStatusStep.ToChain,
|
||||
link: status?.toChain?.transactionUrl,
|
||||
},
|
||||
],
|
||||
[status]
|
||||
);
|
||||
|
||||
if (!status) return <LoadingDots size={3} />;
|
||||
|
||||
return (
|
||||
<Styled.BridgingStatus>
|
||||
{steps.map((step) => (
|
||||
<Styled.Step key={step.step}>
|
||||
<Styled.row>
|
||||
{step.step === currentStep ? (
|
||||
<Styled.Icon>
|
||||
<Styled.Spinner />
|
||||
</Styled.Icon>
|
||||
) : step.step < currentStep ? (
|
||||
<Styled.Icon state="complete">
|
||||
<Icon iconName={IconName.Check} />
|
||||
</Styled.Icon>
|
||||
) : (
|
||||
<Styled.Icon state="default">{step.step + 1}</Styled.Icon>
|
||||
)}
|
||||
{step.link && currentStep >= step.step ? (
|
||||
<Link href={step.link}>
|
||||
<Styled.Label highlighted={currentStep >= step.step}>
|
||||
{step.label}
|
||||
<Icon iconName={IconName.LinkOut} />
|
||||
</Styled.Label>
|
||||
</Link>
|
||||
) : (
|
||||
<Styled.Label highlighted={currentStep >= step.step}>
|
||||
{step.label}
|
||||
</Styled.Label>
|
||||
)}
|
||||
</Styled.row>
|
||||
</Styled.Step>
|
||||
))}
|
||||
</Styled.BridgingStatus>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.BridgingStatus = styled.div`
|
||||
${layoutMixins.flexColumn};
|
||||
|
||||
gap: 1rem;
|
||||
padding: 1rem 0;
|
||||
`;
|
||||
|
||||
Styled.Step = styled.div`
|
||||
${layoutMixins.spacedRow};
|
||||
`;
|
||||
|
||||
Styled.row = styled.div`
|
||||
${layoutMixins.inlineRow};
|
||||
gap: 0.5rem;
|
||||
`;
|
||||
|
||||
Styled.Icon = styled.div<{ state: 'complete' | 'default' }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
|
||||
border-radius: 50%;
|
||||
|
||||
background-color: var(--color-layer-3);
|
||||
|
||||
${({ state }) =>
|
||||
({
|
||||
['complete']: css`
|
||||
color: var(--color-positive);
|
||||
`,
|
||||
['default']: css`
|
||||
color: var(--color-text-0);
|
||||
`,
|
||||
}[state])}
|
||||
`;
|
||||
|
||||
Styled.Spinner = styled(LoadingSpinner)`
|
||||
--spinner-width: 1.25rem;
|
||||
|
||||
color: var(--color-accent);
|
||||
`;
|
||||
|
||||
Styled.Label = styled(Styled.row)<{ highlighted?: boolean }>`
|
||||
${({ highlighted }) =>
|
||||
highlighted
|
||||
? css`
|
||||
color: var(--color-text-2);
|
||||
`
|
||||
: css`
|
||||
color: var(--color-text-0);
|
||||
`}
|
||||
`;
|
||||
@@ -5,11 +5,7 @@ import { useDispatch, shallowEqual, useSelector } from 'react-redux';
|
||||
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client';
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
import {
|
||||
TransferInputField,
|
||||
TransferInputTokenResource,
|
||||
TransferType,
|
||||
} from '@/constants/abacus';
|
||||
import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus';
|
||||
import { AlertType } from '@/constants/alerts';
|
||||
import { ButtonSize } from '@/constants/buttons';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
@@ -58,8 +54,14 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
|
||||
|
||||
const { addTransferNotification } = useNotifications();
|
||||
|
||||
const { requestPayload, token, chain: chainIdStr, resources, summary } = useSelector(getTransferInputs, shallowEqual) || {};
|
||||
const chainId = chainIdStr ? parseInt(chainIdStr) : undefined
|
||||
const {
|
||||
requestPayload,
|
||||
token,
|
||||
chain: chainIdStr,
|
||||
resources,
|
||||
summary,
|
||||
} = useSelector(getTransferInputs, shallowEqual) || {};
|
||||
const chainId = chainIdStr ? parseInt(chainIdStr) : undefined;
|
||||
|
||||
// User inputs
|
||||
const sourceToken = useMemo(
|
||||
@@ -104,13 +106,6 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
|
||||
value: TransferType.deposit.rawValue,
|
||||
});
|
||||
|
||||
addTransferNotification({
|
||||
txHash: '0x4a8bb0893f0bfe5b6bf4eab9ee1ff4a02dd088e9946f3404a6b9e9df5205b057',
|
||||
toChainId: TESTNET_CHAIN_ID,
|
||||
fromChainId: '5',
|
||||
toAmount: 2.5832,
|
||||
});
|
||||
|
||||
return () => {
|
||||
abacusStateManager.clearTransferInputValues();
|
||||
abacusStateManager.setTransferValue({
|
||||
@@ -206,15 +201,9 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
|
||||
fromChainId: chainIdStr || undefined,
|
||||
toAmount: summary?.usdcSize || undefined,
|
||||
});
|
||||
// abacusStateManager.setTransferStatus({
|
||||
// hash: txHash,
|
||||
// toChainId: TESTNET_CHAIN_ID,
|
||||
// fromChainId: chainId?.toString(),
|
||||
// });
|
||||
abacusStateManager.clearTransferInputValues();
|
||||
setFromAmount('');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
} finally {
|
||||
@@ -303,17 +292,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
|
||||
}
|
||||
/>
|
||||
</Styled.WithDetailsReceipt>
|
||||
{errorMessage ? (
|
||||
<AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>
|
||||
) : (
|
||||
transactionHash && (
|
||||
<AlertMessage type={AlertType.Success}>
|
||||
<Styled.TransactionInfo>
|
||||
{stringGetter({ key: STRING_KEYS.DEPOSIT_IN_PROGRESS })}
|
||||
</Styled.TransactionInfo>
|
||||
</AlertMessage>
|
||||
)
|
||||
)}
|
||||
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>}
|
||||
<DepositButtonAndReceipt
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { STRING_KEYS } from '@/constants/localization';
|
||||
import { NumberSign, QUANTUM_MULTIPLIER } from '@/constants/numbers';
|
||||
|
||||
import { useDebounce, useStringGetter, useSubaccount } from '@/hooks';
|
||||
import { useNotifications } from '@/hooks/useNotifications';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
@@ -66,6 +67,8 @@ export const WithdrawForm = () => {
|
||||
[token, resources]
|
||||
);
|
||||
|
||||
const { addTransferNotification } = useNotifications();
|
||||
|
||||
// Async Data
|
||||
const [transactionHash, setTransactionHash] = useState<string>();
|
||||
|
||||
@@ -137,10 +140,12 @@ export const WithdrawForm = () => {
|
||||
const hash = `0x${Buffer.from(txHash.hash).toString('hex')}`;
|
||||
|
||||
setTransactionHash(hash);
|
||||
abacusStateManager.setTransferStatus({
|
||||
hash,
|
||||
|
||||
addTransferNotification({
|
||||
txHash: hash,
|
||||
fromChainId: TESTNET_CHAIN_ID,
|
||||
toChainId: chainIdStr || undefined,
|
||||
toAmount: debouncedAmountBN.toNumber(),
|
||||
});
|
||||
abacusStateManager.clearTransferInputValues();
|
||||
setWithdrawAmount('');
|
||||
|
||||
@@ -89,9 +89,9 @@ export const NotificationsMenu = ({
|
||||
.map(({ notification, key, displayData }) => ({
|
||||
value: key,
|
||||
label: displayData.title ?? '',
|
||||
description: displayData.description,
|
||||
slotBefore: displayData.icon,
|
||||
slotAfter: (
|
||||
description: displayData.customMenuContent || displayData.description,
|
||||
slotBefore: !displayData.customMenuContent && displayData.icon,
|
||||
slotAfter: !displayData.customMenuContent && (
|
||||
<>
|
||||
<$Output
|
||||
type={OutputType.RelativeTime}
|
||||
|
||||
Reference in New Issue
Block a user