diff --git a/package.json b/package.json index 355ff83..ae82166 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@cosmjs/proto-signing": "^0.31.0", "@cosmjs/stargate": "^0.31.0", "@cosmjs/tendermint-rpc": "^0.31.0", - "@dydxprotocol/v4-abacus": "^0.6.4", + "@dydxprotocol/v4-abacus": "^0.7.0", "@dydxprotocol/v4-client-js": "^0.36.1", "@dydxprotocol/v4-localization": "^0.1.30", "@ethersproject/providers": "^5.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e01552..541ef18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ dependencies: specifier: ^0.31.0 version: 0.31.0 '@dydxprotocol/v4-abacus': - specifier: ^0.6.4 - version: 0.6.4 + specifier: ^0.7.0 + version: 0.7.0 '@dydxprotocol/v4-client-js': specifier: ^0.36.1 version: 0.36.1 @@ -979,8 +979,8 @@ packages: resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==} dev: true - /@dydxprotocol/v4-abacus@0.6.4: - resolution: {integrity: sha512-eH+/9Q/OnQtwzBwM1BJyXybFPdivGmFQWrxM71MFWGs6uIQGbZX775mpoa8deweSTyQ/QrRLRQ1mP2BPDgBQYg==} + /@dydxprotocol/v4-abacus@0.7.0: + resolution: {integrity: sha512-XheqfIpOODfQFG860oH0fneVgp9qQKPMDTwrvFXaM/JtKH9ekOUUO6De72EsJ7qOuxWwl1WgoALA8b4tRs0tiw==} dev: false /@dydxprotocol/v4-client-js@0.36.1: diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 935bd3c..5a36f1d 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -6,6 +6,8 @@ import { AddressConnectorIcon, ArrowIcon, Bar3Icon, + BellIcon, + BellStrokeIcon, BoxCloseIcon, CalculatorIcon, CaretIcon, @@ -71,6 +73,8 @@ export enum IconName { AddressConnector = 'AddressConnector', Arrow = 'Arrow', Bar3 = 'Bar3', + Bell = 'Bell', + BellStroked = 'BellStroked', BoxClose = 'BoxClose', Calculator = 'Calculator', Caret = 'Caret', @@ -137,6 +141,8 @@ const icons = { [IconName.AddressConnector]: AddressConnectorIcon, [IconName.Arrow]: ArrowIcon, [IconName.Bar3]: Bar3Icon, + [IconName.Bell]: BellIcon, + [IconName.BellStroked]: BellStrokeIcon, [IconName.BoxClose]: BoxCloseIcon, [IconName.Calculator]: CalculatorIcon, [IconName.Caret]: CaretIcon, @@ -213,7 +219,8 @@ export const Icon = styled( iconComponent: Component = iconName && icons[iconName], className, ...props - }: ElementProps & StyleProps) => (Component ? : null) + }: ElementProps & StyleProps) => + Component ? : null )` width: 1em; height: 1em; diff --git a/src/constants/abacus.ts b/src/constants/abacus.ts index 592cb33..4e761e3 100644 --- a/src/constants/abacus.ts +++ b/src/constants/abacus.ts @@ -59,16 +59,6 @@ export const TransactionType = Abacus.exchange.dydx.abacus.protocols.Transaction const transactionTypes = [...TransactionType.values()] as const; export type TransactionTypes = (typeof transactionTypes)[number]; -export type NetworkConfig = { - chainId: string; - indexerUrl: string; - indexerSocketUrl: string; - validatorUrl: string; - faucetUrl?: string | null; -}; - -export type ConnectNetworkEvent = CustomEvent>; - // ------ State ------ export type AbacusApiState = Abacus.exchange.dydx.abacus.state.manager.ApiState; export const AbacusApiStatus = Abacus.exchange.dydx.abacus.state.manager.ApiStatus; @@ -267,3 +257,20 @@ export const TRADE_TYPES: Record< [AbacusOrderType.liquidation.name]: null, [AbacusOrderType.trailingStop.name]: null, }; + +// Custom types involving Abacus + +export type NetworkConfig = Partial<{ + indexerUrl: Nullable; + websocketUrl: Nullable; + validatorUrl: Nullable; + chainId: Nullable; + faucetUrl: Nullable; + USDC_DENOM: Nullable; + USDC_DECIMALS: Nullable; + USDC_GAS_DENOM: Nullable; + CHAINTOKEN_DENOM: Nullable; + CHAINTOKEN_DECIMALS: Nullable; +}>; + +export type ConnectNetworkEvent = CustomEvent>; diff --git a/src/hooks/tradingView/useTradingView.ts b/src/hooks/tradingView/useTradingView.ts index 5206e2f..8a4e8a1 100644 --- a/src/hooks/tradingView/useTradingView.ts +++ b/src/hooks/tradingView/useTradingView.ts @@ -33,7 +33,7 @@ export const useTradingView = ({ const marketIds = useSelector(getMarketIds, shallowEqual); const selectedLocale = useSelector(getSelectedLocale); const selectedNetwork = useSelector(getSelectedNetwork); - const { getCandlesForDatafeed } = useDydxClient(); + const { getCandlesForDatafeed, isConnected: isClientConnected } = useDydxClient(); const [savedTvChartConfig, setTvChartConfig] = useLocalStorage({ key: LocalStorageKey.TradingViewChartConfig, @@ -44,7 +44,7 @@ export const useTradingView = ({ const hasMarkets = marketIds.length > 0; useEffect(() => { - if (hasMarkets) { + if (hasMarkets && isClientConnected) { const widgetOptions = getWidgetOptions(); const widgetOverrides = getWidgetOverrides(appTheme); const options = { @@ -75,7 +75,7 @@ export const useTradingView = ({ tvWidgetRef.current = null; setIsChartReady(false); }; - }, [getCandlesForDatafeed, hasMarkets, selectedLocale, selectedNetwork]); + }, [getCandlesForDatafeed, isClientConnected, hasMarkets, selectedLocale, selectedNetwork]); return { savedResolution }; }; diff --git a/src/hooks/useCurrentMarketId.ts b/src/hooks/useCurrentMarketId.ts index 087ef9c..6026c88 100644 --- a/src/hooks/useCurrentMarketId.ts +++ b/src/hooks/useCurrentMarketId.ts @@ -44,6 +44,9 @@ export const useCurrentMarketId = () => { }, [validId]); useEffect(() => { - abacusStateManager.setMarket(marketId ?? DEFAULT_MARKETID); - }, [selectedNetwork, marketId]); + // Check for marketIds otherwise Abacus will silently fail its isMarketValid check + if (marketIds) { + abacusStateManager.setMarket(marketId ?? DEFAULT_MARKETID); + } + }, [selectedNetwork, marketIds, marketId]); }; diff --git a/src/hooks/useDydxClient.tsx b/src/hooks/useDydxClient.tsx index b8202ff..f605000 100644 --- a/src/hooks/useDydxClient.tsx +++ b/src/hooks/useDydxClient.tsx @@ -13,7 +13,7 @@ import { import type { ResolutionString } from 'public/tradingview/charting_library'; -import type { NetworkConfig, ConnectNetworkEvent } from '@/constants/abacus'; +import type { ConnectNetworkEvent, NetworkConfig } from '@/constants/abacus'; import { type Candle, RESOLUTION_MAP } from '@/constants/candles'; import { getSelectedNetwork } from '@/state/appSelectors'; @@ -37,7 +37,7 @@ const useDydxClientContext = () => { const selectedNetwork = useSelector(getSelectedNetwork); - const [networkConfig, setNetworkConfig] = useState>(); + const [networkConfig, setNetworkConfig] = useState(); useEffect(() => { const onConnectNetwork = (event: ConnectNetworkEvent) => setNetworkConfig(event.detail); @@ -57,14 +57,14 @@ const useDydxClientContext = () => { if ( networkConfig?.chainId && networkConfig?.indexerUrl && - networkConfig?.indexerSocketUrl && + networkConfig?.websocketUrl && networkConfig?.validatorUrl ) { try { const initializedClient = await CompositeClient.connect( new Network( selectedNetwork, - new IndexerConfig(networkConfig.indexerUrl, networkConfig.indexerSocketUrl), + new IndexerConfig(networkConfig.indexerUrl, networkConfig.websocketUrl), new ValidatorConfig(networkConfig.validatorUrl, networkConfig.chainId, { broadcastPollIntervalMs: 3_000, broadcastTimeoutMs: 60_000, @@ -209,6 +209,7 @@ const useDydxClientContext = () => { networkConfig, compositeClient, faucetClient, + isConnected: !!compositeClient, // Wallet Methods getWalletFromEvmSignature, diff --git a/src/hooks/useNotificationTypes.tsx b/src/hooks/useNotificationTypes.tsx index c09d7c9..5f5a8d3 100644 --- a/src/hooks/useNotificationTypes.tsx +++ b/src/hooks/useNotificationTypes.tsx @@ -4,15 +4,14 @@ import { useSelector, shallowEqual, useDispatch } from 'react-redux'; import { groupBy } from 'lodash'; import { AlertType } from '@/constants/alerts'; -import { AbacusOrderStatus, ORDER_SIDES, ORDER_STATUS_STRINGS } from '@/constants/abacus'; +import { AbacusOrderStatus, ORDER_SIDES } from '@/constants/abacus'; import { DialogTypes } from '@/constants/dialogs'; -import { STRING_KEYS } from '@/constants/localization'; +import { STRING_KEYS, StringKey } from '@/constants/localization'; import { type NotificationTypeConfig, NotificationType } from '@/constants/notifications'; -import { ORDER_SIDE_STRINGS, TRADE_TYPE_STRINGS, TradeTypes } from '@/constants/trade'; +import { ORDER_SIDE_STRINGS } from '@/constants/trade'; import { useLocalNotifications } from '@/hooks/useLocalNotifications'; -import { AlertMessage } from '@/components/AlertMessage'; import { Icon, IconName } from '@/components/Icon'; import { Output, OutputType } from '@/components/Output'; import { TransferStatusToast } from '@/views/TransferStatus'; @@ -69,11 +68,11 @@ export const notificationTypes = [ ), title: `${stringGetter({ - key: TRADE_TYPE_STRINGS[order.type.rawValue as TradeTypes].tradeTypeKey, + key: order.resources.typeStringKey as StringKey, })} ${ order.status === AbacusOrderStatus.open && (order?.totalFilled ?? 0) > 0 ? stringGetter({ key: STRING_KEYS.PARTIALLY_FILLED }) - : stringGetter({ key: ORDER_STATUS_STRINGS[order.status.name] }) + : stringGetter({ key: order.resources.statusStringKey as StringKey }) }`, description: `${stringGetter({ key: ORDER_SIDE_STRINGS[ORDER_SIDES[order.side.name]], @@ -91,18 +90,34 @@ export const notificationTypes = [ }, [orderIds]); }, - useNotificationAction: () => { - const dispatch = useDispatch(); + // useNotificationAction: () => { + // const dispatch = useDispatch(); + // const orders = useSelector(getSubaccountOrders, shallowEqual) || []; + // const ordersByOrderId = Object.fromEntries(orders.map((order) => [order.id, order])); - return (orderId) => { - dispatch( - openDialog({ - type: DialogTypes.OrderDetails, - dialogProps: { orderId }, - }) - ); - }; - }, + // const fills = useSelector(getSubaccountFills, shallowEqual) || []; + // const fillsByOrderId = groupBy(fills, (fill) => fill.orderId); + + // return (id) => { + // if (ordersByOrderId[id]) { + // dispatch( + // openDialog({ + // type: DialogTypes.OrderDetails, + // dialogProps: { orderId: id }, + // }) + // ); + // } else if (fillsByOrderId[id]) { + // const fillId = fillsByOrderId[id][0].id; + + // dispatch( + // openDialog({ + // type: DialogTypes.FillDetails, + // dialogProps: { fillId }, + // }) + // ); + // } + // }; + // }, } as NotificationTypeConfig, { type: NotificationType.SquidTransfer, @@ -138,7 +153,8 @@ export const notificationTypes = [ {stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE, params: { - ERROR_MESSAGE: error.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }), + ERROR_MESSAGE: + error.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }), }, })} @@ -182,8 +198,8 @@ Styled.TransferText = styled.span` display: inline-flex; align-items: center; gap: 0.5ch; -` +`; Styled.ErrorMessage = styled.div` max-width: 13rem; -`; \ No newline at end of file +`; diff --git a/src/icons/bell-stroke.svg b/src/icons/bell-stroke.svg new file mode 100644 index 0000000..21ecaa7 --- /dev/null +++ b/src/icons/bell-stroke.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/bell.svg b/src/icons/bell.svg index f714c96..3fa83d3 100644 --- a/src/icons/bell.svg +++ b/src/icons/bell.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/icons/index.ts b/src/icons/index.ts index a3a2bf8..a96402a 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -2,6 +2,7 @@ export { default as AddressConnectorIcon } from './address-connector.svg'; export { default as ArrowIcon } from './arrow.svg'; export { default as Bar3Icon } from './bar3.svg'; export { default as BellIcon } from './bell.svg'; +export { default as BellStrokeIcon } from './bell-stroke.svg'; export { default as BoxCloseIcon } from './box-close.svg'; export { default as CalculatorIcon } from './calculator.svg'; export { default as CaretIcon } from './caret-down.svg'; diff --git a/src/layout/Header/HeaderDesktop.tsx b/src/layout/Header/HeaderDesktop.tsx index 20ceb75..f637a60 100644 --- a/src/layout/Header/HeaderDesktop.tsx +++ b/src/layout/Header/HeaderDesktop.tsx @@ -7,7 +7,7 @@ import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { AppRoute } from '@/constants/routes'; import { useStringGetter } from '@/hooks'; -import { LogoShortIcon, BellIcon } from '@/icons'; +import { LogoShortIcon, BellStrokeIcon } from '@/icons'; import { Icon, IconName } from '@/components/Icon'; import { IconButton } from '@/components/IconButton'; @@ -131,7 +131,9 @@ export const HeaderDesktop = () => { } + slotTrigger={ + + } /> @@ -165,7 +167,7 @@ Styled.Header = styled.header` ) var(--border-width) 1fr var(--border-width) auto; - font-size: 0.9375em; + font-size: 0.9375rem; :before { backdrop-filter: blur(10px); @@ -214,7 +216,8 @@ Styled.NavAfter = styled.div` } `; -Styled.IconButton = styled(IconButton)` +Styled.IconButton = styled(IconButton)<{ size?: string }>` ${headerMixins.button} --button-border: none; + --button-icon-size: 1rem; `; diff --git a/src/lib/abacus/dydxChainTransactions.ts b/src/lib/abacus/dydxChainTransactions.ts index e82a3d0..45fc8a4 100644 --- a/src/lib/abacus/dydxChainTransactions.ts +++ b/src/lib/abacus/dydxChainTransactions.ts @@ -61,18 +61,17 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { } async connectNetwork( - indexerUrl: string, - indexerSocketUrl: string, - validatorUrl: string, - chainId: string, - faucetUrl: Nullable | undefined, + paramsInJson: Nullable, callback: (p0: Nullable) => void ): Promise { try { + const parsedParams = paramsInJson ? JSON.parse(paramsInJson) : {}; + const { indexerUrl, websocketUrl, validatorUrl, chainId } = parsedParams; + const compositeClient = await CompositeClient.connect( new Network( chainId, - new IndexerConfig(indexerUrl, indexerSocketUrl), + new IndexerConfig(indexerUrl, websocketUrl), new ValidatorConfig(validatorUrl, chainId, { broadcastPollIntervalMs: 3_000, broadcastTimeoutMs: 60_000, @@ -84,13 +83,7 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { // Dispatch custom event to notify other parts of the app that the network has been connected const customEvent = new CustomEvent('abacus:connectNetwork', { - detail: { - indexerUrl, - indexerSocketUrl, - validatorUrl, - chainId, - faucetUrl, - }, + detail: parsedParams, }); globalThis.dispatchEvent(customEvent); @@ -99,9 +92,7 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { this.store?.dispatch( openDialog({ type: DialogTypes.ExchangeOffline, dialogProps: { preventClose: true } }) ); - log('DydxChainTransactions/connectNetwork', error); - return; } } @@ -251,7 +242,7 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { const msg = compositeClient.withdrawFromSubaccountMessage(subaccountClient, amount); resolve([msg]); - }), + }) ); const parsedTx = this.parseToPrimitives(tx); @@ -266,7 +257,9 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { } } - async simulateTransferNativeTokenTransaction(params: HumanReadableTransferPayload): Promise { + async simulateTransferNativeTokenTransaction( + params: HumanReadableTransferPayload + ): Promise { if (!this.compositeClient || !this.localWallet) { throw new Error('Missing compositeClient or localWallet'); } @@ -289,7 +282,7 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { resolve([msg]); }), - GAS_PRICE_DYDX_DENOM, + GAS_PRICE_DYDX_DENOM ); const parsedTx = this.parseToPrimitives(tx); @@ -397,9 +390,10 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { break; case QueryType.GetAccountBalances: if (!this.localWallet?.address) throw new Error('Missing localWallet'); - const accountBalances = await this.compositeClient?.validatorClient.get.getAccountBalances( - this.localWallet.address - ); + const accountBalances = + await this.compositeClient?.validatorClient.get.getAccountBalances( + this.localWallet.address + ); const parsedAccountBalances = this.parseToPrimitives(accountBalances); callback(JSON.stringify(parsedAccountBalances)); break; diff --git a/src/views/forms/TradeForm/AdvancedTradeOptions.tsx b/src/views/forms/TradeForm/AdvancedTradeOptions.tsx index a1e1842..40a5282 100644 --- a/src/views/forms/TradeForm/AdvancedTradeOptions.tsx +++ b/src/views/forms/TradeForm/AdvancedTradeOptions.tsx @@ -29,7 +29,7 @@ export const AdvancedTradeOptions = () => { const currentTradeFormConfig = useSelector(getInputTradeOptions, shallowEqual); const inputTradeData = useSelector(getInputTradeData, shallowEqual); - const { execution, goodTil, postOnly, reduceOnly, timeInForce } = inputTradeData || {}; + const { execution, goodTil, postOnly, reduceOnly, timeInForce, type } = inputTradeData || {}; const { executionOptions, needsGoodUntil, needsPostOnly, needsReduceOnly, timeInForceOptions } = currentTradeFormConfig || {}; @@ -37,6 +37,7 @@ export const AdvancedTradeOptions = () => { const { duration, unit } = goodTil || {}; const needsExecution = executionOptions || needsPostOnly || needsReduceOnly; + const hasTimeInForce = timeInForceOptions?.toArray()?.length; return ( { > - {timeInForceOptions?.toArray() && ( + {hasTimeInForce && ( @@ -72,7 +73,9 @@ export const AdvancedTradeOptions = () => { id="trade-good-til-time" type={InputType.Number} decimals={INTEGER_DECIMALS} - label={stringGetter({ key: STRING_KEYS.TIME })} + label={stringGetter({ + key: hasTimeInForce ? STRING_KEYS.TIME : STRING_KEYS.GOOD_TIL_TIME, + })} onChange={({ value }: NumberFormatValues) => { abacusStateManager.setTradeValue({ value: Number(value), diff --git a/src/views/menus/NotificationsMenu.tsx b/src/views/menus/NotificationsMenu.tsx index 348f7d4..842e128 100644 --- a/src/views/menus/NotificationsMenu.tsx +++ b/src/views/menus/NotificationsMenu.tsx @@ -178,10 +178,11 @@ export const NotificationsMenu = ({ }; const $UnreadIndicator = styled.div` - width: 0.5em; - height: 0.5em; + width: 0.5rem; + height: 0.5rem; border-radius: 50%; background-color: var(--color-accent); + border: 1px solid var(--color-layer-2); `; const $TriggerContainer = styled.div` @@ -192,8 +193,8 @@ const $TriggerUnreadIndicator = styled($UnreadIndicator)` place-self: center; position: relative; - right: -0.425em; - top: -0.425em; + right: -0.2rem; + top: -0.325rem; `; const $Output = styled(Output)`