Compare commits

..
Author SHA1 Message Date
Bill He d7bb026af4 address comments 2023-09-05 12:09:15 -07:00
Bill He 1af60835bf wait for approval 2023-09-05 12:00:05 -07:00
Bill He c0acb3fac3 update lock 2023-09-05 11:07:37 -07:00
Bill He 1608a27e3e address comments 2023-09-05 10:59:44 -07:00
Bill He 9f32c6b0f2 check for token approval 2023-09-05 10:58:33 -07:00
Bill He 74e9dfc132 fix merge 2023-09-05 10:58:33 -07:00
Bill He 6345df1b2a prevent log out during switch accounts 2023-09-05 10:58:33 -07:00
Bill He 311490f144 Deposit/Withdraw status notifications 2023-09-05 10:58:33 -07:00
28 changed files with 99 additions and 119 deletions
+1 -1
View File
@@ -37,7 +37,7 @@
"@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0",
"@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/abacus": "^0.4.8",
"@dydxprotocol/abacus": "^0.4.7",
"@dydxprotocol/v4-client-js": "^0.32.0",
"@dydxprotocol/v4-localization": "^0.0.25",
"@ethersproject/providers": "^5.7.2",
+4 -4
View File
@@ -27,8 +27,8 @@ dependencies:
specifier: ^0.31.0
version: 0.31.0
'@dydxprotocol/abacus':
specifier: ^0.4.8
version: 0.4.8
specifier: ^0.4.7
version: 0.4.7
'@dydxprotocol/v4-client-js':
specifier: ^0.32.0
version: 0.32.0
@@ -1145,8 +1145,8 @@ packages:
resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==}
dev: true
/@dydxprotocol/abacus@0.4.8:
resolution: {integrity: sha512-hX31m+a5Bwbs4dr5raPDtnflwUzEu/9BIt+LOlQgt1YOViuIse8dozgpgCHvqvA8MttqY3QGPalx5iRB8j8vtw==}
/@dydxprotocol/abacus@0.4.7:
resolution: {integrity: sha512-3c7Dx5H+KeXZYDpAOJs6fqxSNerKqNSD4tuMNn5F7RXAPWUOU8ADQHAFPnrP1PgTxPjfZBZQzLIziKpyXww4NQ==}
dev: false
/@dydxprotocol/dydxjs@0.3.0:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

+4
View File
@@ -80,6 +80,8 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
{group.items.map((item) => (
<Fragment key={item.value}>
<Styled.Item
// value={item.value} // search by both value and textContent
// value={[group.groupLabel, item.label, item.tag].filter(Boolean).join('|')} // exclude item.value from searchable terms (not guaranteed to be unique)
value={[group.groupLabel, item.value, item.description, item.label, item.tag]
.filter(Boolean)
.join('|')}
@@ -114,6 +116,8 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
item.subitems?.map((subitem) => (
<Fragment key={subitem.value}>
<Styled.Item
// value={subitem.value} // search by both value and textContent
// value={[group.groupLabel, item.label, subitem.label, subitem.tag].filter(Boolean).join('|')}
value={[
group.groupLabel,
item.value,
+1 -4
View File
@@ -40,10 +40,7 @@ export type AbacusFileSystemProtocol = Omit<
Abacus.exchange.dydx.abacus.protocols.FileSystemProtocol,
'__doNotUseOrImplementIt'
>;
export type AbacusTrackingProtocol = Omit<
Abacus.exchange.dydx.abacus.protocols.TrackingProtocol,
'__doNotUseOrImplementIt'
>;
export type FileLocation = Abacus.exchange.dydx.abacus.protocols.FileLocation;
export type ThreadingType = Abacus.exchange.dydx.abacus.protocols.ThreadingType;
export const CoroutineTimer = Abacus.exchange.dydx.abacus.utils.CoroutineTimer;
-2
View File
@@ -34,8 +34,6 @@ export const STRING_KEYS = {
...WARNINGS_STRING_KEYS,
};
export type StringKey = keyof typeof STRING_KEYS;
export type LocaleData = typeof EN_LOCALE_DATA;
export type StringGetterFunction = (a: {
+11 -17
View File
@@ -1,5 +1,4 @@
import type { ReactNode } from 'react';
import type { StatusResponse } from '@0xsquid/sdk';
import { StatusResponse } from "@0xsquid/sdk";
/** implemented in useNotificationTypes */
export enum NotificationType {
@@ -19,31 +18,26 @@ export type NotificationTypeConfig<
/** React hook to trigger notifications based on app state */
useTrigger: (_: {
trigger: ({
id,
displayData,
updateKey,
isNew,
}: {
trigger: (
/** Unique ID for the triggered notification */
id: _NotificationId;
id: _NotificationId,
/** Display data for the triggered notification */
displayData: NotificationDisplayData;
displayData: NotificationDisplayData,
/**
* JSON-serializable key.
* Re-triggers the notification if passed a different value from the last trigger() call (even from a previous browser session).
* Suggested usage: data dependency array
*/
updateKey?: NotificationUpdateKey;
updateKey?: NotificationUpdateKey,
/**
* @param true (default): Notification initialized with status NotificationStatus.Triggered
* @param false: Notification initialized with status NotificationStatus.Cleared
*/
isNew?: boolean;
}) => void;
isNew?: boolean
) => void;
lastUpdated: number;
}) => void;
@@ -85,15 +79,15 @@ export type Notifications = Record<NotificationId, Notification<any>>;
/** Notification display data derived from app state at runtime. */
export type NotificationDisplayData = {
icon?: React.ReactElement<any, 'svg'>;
icon?: React.ReactNode;
title?: string;
description?: ReactNode;
description?: React.ReactNode;
customContent?: ReactNode;
customContent?: React.ReactNode;
customMenuContent?: ReactNode;
customMenuContent?: React.ReactNode;
actionDescription?: string;
+39 -38
View File
@@ -1,27 +1,27 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useSelector, shallowEqual, useDispatch } from 'react-redux';
import { groupBy } from 'lodash';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
import { AbacusOrderStatus, ORDER_SIDES, ORDER_STATUS_STRINGS } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
import { type NotificationTypeConfig, NotificationType } from '@/constants/notifications';
import { ORDER_SIDE_STRINGS } from '@/constants/trade';
import { ORDER_SIDE_STRINGS, TRADE_TYPE_STRINGS, TradeTypes } from '@/constants/trade';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { AssetIcon } from '@/components/AssetIcon';
import { Icon, IconName } from '@/components/Icon';
import { Output, OutputType } from '@/components/Output';
import { OrderStatusIcon } from '@/views/OrderStatusIcon';
import { TransferStatusToast } from '@/views/TransferStatus';
import { TransferStatusSteps } from '@/views/TransferStatusSteps';
import { getSubaccountFills, getSubaccountOrders } from '@/state/accountSelectors';
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-js';
export const notificationTypes = [
{
@@ -58,33 +58,34 @@ export const notificationTypes = [
}
: undefined);
if (order) console.log(order);
trigger({
id: order.id,
displayData: {
icon: <AssetIcon symbol={order.marketId.split('-')[0]} />, //<OrderStatusIcon status={order.status} totalFilled={order.totalFilled ?? 0} />,
title: `${stringGetter({
key: order.resources.typeStringKey ?? '',
})} ${
order.status === AbacusOrderStatus.open && (order?.totalFilled ?? 0) > 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: Infinity,
},
updateKey: [order.status.name, order.size],
isNew: !order.createdAtMilliseconds || order.createdAtMilliseconds > lastUpdated,
});
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) > 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, stringGetter]);
}, [orderIds]);
},
useNotificationAction: () => {
@@ -119,9 +120,9 @@ export const notificationTypes = [
const finished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
const type = toChainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdraw';
trigger({
id: txHash,
displayData: {
trigger(
txHash,
{
icon: <Icon iconName={finished ? IconName.Transfer : IconName.Clock} />,
title: stringGetter({ key: getTitleStringKey(type, finished) }),
// TODO: confirm with design what the description should be
@@ -141,10 +142,10 @@ export const notificationTypes = [
customMenuContent: !finished && <TransferStatusSteps status={transfer.status} />,
toastSensitivity: 'foreground',
},
updateKey: [],
});
[]
);
}
}, [transferNotifications, stringGetter]);
}, [transferNotifications]);
},
} as NotificationTypeConfig<string, []>,
},
] satisfies NotificationTypeConfig[];
+22 -17
View File
@@ -92,7 +92,7 @@ const useNotificationsContext = () => {
for (const { type, useTrigger } of notificationTypes)
useTrigger({
trigger: useCallback(
({ id, displayData, updateKey, isNew = true }) => {
(id, displayData, updateKey, isNew = true) => {
const key = getKey({ type, id });
const notification = notifications[key];
@@ -182,23 +182,28 @@ const useNotificationsContext = () => {
const iconUrl =
displayData.icon && (await renderSvgToDataUrl(displayData.icon).catch(() => undefined));
if (displayData.title) {
const pushNotification = new globalThis.Notification(displayData.title, {
renotify: true,
tag: getKey(notification),
data: notification,
body: displayData.description ?? '',
icon: (iconUrl as string) ?? '/favicon.svg',
badge: (iconUrl as string) ?? '/favicon.svg',
image: (iconUrl as string) ?? '/favicon.svg',
requireInteraction: displayData.toastDuration === Infinity,
});
const pushNotification = new globalThis.Notification(displayData.title, {
renotify: true,
tag: getKey(notification),
data: notification,
body: displayData.description,
icon: iconUrl ?? '/favicon.svg',
badge: iconUrl ?? '/favicon.svg',
image: iconUrl ?? '/favicon.svg',
vibrate: displayData.toastSensitivity === 'foreground',
requireInteraction: displayData.toastDuration === Infinity,
// actions: [
// {
// action: displayData.actionDescription,
// title: displayData.actionDescription,
// }
// ].slice(0, globalThis.Notification.maxActions),
});
pushNotification.addEventListener('click', () => {
onNotificationAction(notification);
markSeen(notification);
});
}
pushNotification.addEventListener('click', () => {
onNotificationAction(notification);
markSeen(notification);
});
}
setPushNotificationsLastUpdated(Date.now());
+1 -2
View File
@@ -36,7 +36,6 @@ import AbacusStateNotifier from './stateNotification';
import AbacusLocalizer from './localizer';
import AbacusFormatter from './formatter';
import AbacusThreading from './threading';
import AbacusTracking from './tracking';
import AbacusFileSystem, { ENDPOINTS_PATH } from './filesystem';
import { LocaleSeparators } from '../numbers';
class AbacusStateManager {
@@ -60,7 +59,7 @@ class AbacusStateManager {
new AbacusRest(),
this.websocket,
new AbacusChainTransaction(),
new AbacusTracking(),
null,
new AbacusThreading(),
new CoroutineTimer(),
new AbacusFileSystem()
-9
View File
@@ -1,9 +0,0 @@
import type { AbacusTrackingProtocol, Nullable } from '@/constants/abacus';
class AbacusTracking implements AbacusTrackingProtocol {
log(event: string, data: Nullable<string>) {
console.log({ event, data });
}
}
export default AbacusTracking;
+2 -4
View File
@@ -21,10 +21,8 @@ const applyComputedStyles = (html: string) => {
const toDataUrl = (bytes: string, type = 'image/svg+xml') =>
new Promise<string | ArrayBuffer | null>((resolve, reject) => {
Object.assign(new FileReader(), {
onload: (e: {
target: { result: string | ArrayBuffer | PromiseLike<string | ArrayBuffer | null> | null };
}) => resolve(e.target.result),
onerror: (e: { target: { error: Error } }) => reject(e.target.error),
onload: (e) => resolve(e.target.result),
onerror: (e) => reject(e.target.error),
}).readAsDataURL(new File([bytes], '', { type }));
});
@@ -7,7 +7,7 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { AbacusOrderStatus, AbacusOrderTypes, type Nullable } from '@/constants/abacus';
import { ButtonAction } from '@/constants/buttons';
import { STRING_KEYS, StringKey } from '@/constants/localization';
import { STRING_KEYS } from '@/constants/localization';
import { AssetIcon } from '@/components/AssetIcon';
import { Button } from '@/components/Button';
@@ -38,9 +38,6 @@ type ElementProps = {
setIsOpen: (open: boolean) => void;
};
const isStringKey = (key: Nullable<string>): key is StringKey =>
key != null && STRING_KEYS.hasOwnProperty(key);
export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
const stringGetter = useStringGetter();
const dispatch = useDispatch();
@@ -50,7 +47,7 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
const { cancelOrder } = useSubaccount();
const {
asset,
asset = {},
cancelReason,
createdAtMilliseconds,
expiresAtMilliseconds,
@@ -60,7 +57,7 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
price,
reduceOnly,
totalFilled,
resources,
resources = {},
size,
status,
stepSizeDecimals,
@@ -112,9 +109,9 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
<Styled.Row>
<Styled.StatusIcon iconName={statusIcon} color={statusIconColor} />
<Styled.Status>
{isStringKey(statusStringKey)
{statusStringKey
? stringGetter({ key: statusStringKey })
: isStringKey(resources.statusStringKey)
: resources.statusStringKey
? stringGetter({ key: resources.statusStringKey })
: undefined}
</Styled.Status>
@@ -124,9 +121,7 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
{
key: 'cancel-reason',
label: stringGetter({ key: STRING_KEYS.CANCEL_REASON }),
value: isStringKey(cancelReason)
? stringGetter({ key: STRING_KEYS[cancelReason] })
: undefined,
value: cancelReason ? stringGetter({ key: STRING_KEYS[cancelReason] }) : undefined,
},
{
key: 'amount',
+8 -10
View File
@@ -1,12 +1,10 @@
import React, { useMemo } from 'react';
import { groupBy } from 'lodash';
import styled from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
import { groupBy } from 'lodash';
import { type Notification, NotificationStatus } from '@/constants/notifications';
import { useStringGetter } from '@/hooks';
import { useNotifications } from '@/hooks/useNotifications';
import { CloseIcon } from '@/icons';
import { Button } from '@/components/Button';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
@@ -15,7 +13,9 @@ import { DialogPlacement } from '@/components/Dialog';
import { Output, OutputType } from '@/components/Output';
import { IconButton } from '@/components/IconButton';
import { Toolbar } from '@/components/Toolbar';
import { CloseIcon } from '@/icons';
import styled from 'styled-components';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
@@ -27,8 +27,6 @@ export const NotificationsMenu = ({
slotTrigger,
placement = DialogPlacement.Sidebar,
}: ElementProps) => {
const stringGetter = useStringGetter();
const {
notifications,
getDisplayData,
@@ -71,7 +69,7 @@ export const NotificationsMenu = ({
.map(([status, notifications]) => ({
group: status,
groupLabel: {
[NotificationStatus.Triggered]: stringGetter({ key: STRING_KEYS.NEW }),
[NotificationStatus.Triggered]: 'New',
// [NotificationStatus.Updated]: 'Updates',
[NotificationStatus.Seen]: 'Seen',
[NotificationStatus.Cleared]: 'Archived',
@@ -137,7 +135,7 @@ export const NotificationsMenu = ({
isOpen={isMenuOpen || placement === DialogPlacement.Inline}
setIsOpen={setIsMenuOpen}
items={items}
title={stringGetter({ key: STRING_KEYS.NOTIFICATIONS })}
title="Notifications"
slotTrigger={
<$TriggerContainer>
{slotTrigger}
@@ -170,7 +168,7 @@ export const NotificationsMenu = ({
),
}}
>
{stringGetter({ key: STRING_KEYS.CLEAR })}
Clear All
</Button>
</$FooterToolbar>
}