Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a123976646 | ||
|
|
788fed9c26 | ||
|
|
564192fa2b | ||
|
|
b8526a5568 | ||
|
|
102b1702ff | ||
|
|
b0cd8e71ac |
@@ -30,14 +30,14 @@
|
||||
},
|
||||
"packageManager": "pnpm@8.6.6",
|
||||
"dependencies": {
|
||||
"@0xsquid/sdk": "^1.7.2",
|
||||
"@0xsquid/sdk": "^1.10.0",
|
||||
"@cosmjs/amino": "^0.31.0",
|
||||
"@cosmjs/crypto": "^0.31.0",
|
||||
"@cosmjs/encoding": "^0.31.0",
|
||||
"@cosmjs/proto-signing": "^0.31.0",
|
||||
"@cosmjs/stargate": "^0.31.0",
|
||||
"@cosmjs/tendermint-rpc": "^0.31.0",
|
||||
"@dydxprotocol/abacus": "^0.4.6",
|
||||
"@dydxprotocol/abacus": "^0.4.8",
|
||||
"@dydxprotocol/v4-client-js": "^0.32.0",
|
||||
"@dydxprotocol/v4-localization": "^0.0.25",
|
||||
"@ethersproject/providers": "^5.7.2",
|
||||
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 817 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 973 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
@@ -13,8 +13,9 @@ import { AccountsProvider } from '@/hooks/useAccounts';
|
||||
import { DialogAreaProvider, useDialogArea } from './hooks/useDialogArea';
|
||||
import { LocaleProvider } from './hooks/useLocaleSeparators';
|
||||
import { NotificationsProvider } from './hooks/useNotifications';
|
||||
import { LocalNotificationsProvider } from './hooks/useLocalNotifications';
|
||||
import { SubaccountProvider } from './hooks/useSubaccount';
|
||||
import { SquidProvider } from '@/hooks/useSquidRouter';
|
||||
import { SquidProvider } from '@/hooks/useSquid';
|
||||
|
||||
import { GuardedMobileRoute } from '@/components/GuardedMobileRoute';
|
||||
|
||||
@@ -98,29 +99,31 @@ const Content = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const App = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<GrazProvider>
|
||||
<WagmiConfig config={config}>
|
||||
<LocaleProvider>
|
||||
<DydxProvider>
|
||||
<AccountsProvider>
|
||||
<SubaccountProvider>
|
||||
<NotificationsProvider>
|
||||
<DialogAreaProvider>
|
||||
<SquidProvider>
|
||||
<Content />
|
||||
</SquidProvider>
|
||||
</DialogAreaProvider>
|
||||
</NotificationsProvider>
|
||||
</SubaccountProvider>
|
||||
</AccountsProvider>
|
||||
</DydxProvider>
|
||||
</LocaleProvider>
|
||||
</WagmiConfig>
|
||||
</GrazProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
const wrapProvider = (Component: React.ComponentType<any>, props?: any) => {
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<Component {...props}>{children}</Component>
|
||||
);
|
||||
};
|
||||
|
||||
const providers = [
|
||||
wrapProvider(QueryClientProvider, { client: queryClient }),
|
||||
wrapProvider(GrazProvider),
|
||||
wrapProvider(WagmiConfig, { config }),
|
||||
wrapProvider(LocaleProvider),
|
||||
wrapProvider(DydxProvider),
|
||||
wrapProvider(AccountsProvider),
|
||||
wrapProvider(SubaccountProvider),
|
||||
wrapProvider(SquidProvider),
|
||||
wrapProvider(LocalNotificationsProvider),
|
||||
wrapProvider(NotificationsProvider),
|
||||
wrapProvider(DialogAreaProvider),
|
||||
];
|
||||
|
||||
const App = () => {
|
||||
return [...providers].reverse().reduce((children, Provider) => {
|
||||
return <Provider>{children}</Provider>;
|
||||
}, <Content />);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "name",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "approve",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "totalSupply",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transferFrom",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "decimals",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint8"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "balance",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transfer",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_spender",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "allowance",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"payable": true,
|
||||
"stateMutability": "payable",
|
||||
"type": "fallback"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Approval",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
}
|
||||
]
|
||||
@@ -80,8 +80,6 @@ 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('|')}
|
||||
@@ -116,8 +114,6 @@ 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,
|
||||
|
||||
@@ -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,12 +80,13 @@ export const Toast = ({
|
||||
|
||||
<$Title>{slotTitle}</$Title>
|
||||
</$Header>
|
||||
|
||||
<$Description>{slotDescription}</$Description>
|
||||
|
||||
<$Action asChild altText={actionAltText}>
|
||||
{slotAction}
|
||||
</$Action>
|
||||
{!slotCustomContent && <$Description>{slotDescription}</$Description>}
|
||||
{slotCustomContent}
|
||||
{actionDescription && (
|
||||
<$Action asChild altText={actionAltText}>
|
||||
{slotAction}
|
||||
</$Action>
|
||||
)}
|
||||
</$Container>
|
||||
</div>
|
||||
</$Root>
|
||||
|
||||
@@ -40,7 +40,10 @@ 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;
|
||||
|
||||
@@ -49,11 +49,11 @@ export enum EvmDerivedAccountStatus {
|
||||
Derived,
|
||||
}
|
||||
|
||||
import type { DydxAddress, EthereumAddress } from './wallets';
|
||||
import type { DydxAddress, EvmAddress } from './wallets';
|
||||
|
||||
export type EvmDerivedAddresses = {
|
||||
version?: string;
|
||||
[EthereumAddress: EthereumAddress]: {
|
||||
[EvmAddress: EvmAddress]: {
|
||||
encryptedSignature?: string;
|
||||
dydxAddress?: DydxAddress;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { SupportedLocales } from './localization';
|
||||
import type { DydxNetwork } from './networks';
|
||||
import type { OnboardingState, OnboardingSteps } from './account';
|
||||
import type { DydxAddress, WalletType, WalletConnectionType, EthereumAddress } from './wallets';
|
||||
import type { DydxAddress, WalletType, WalletConnectionType, EvmAddress } from './wallets';
|
||||
import type { DialogTypes } from './dialogs';
|
||||
import type { TradeTypes } from './trade';
|
||||
import type { AbacusApiStatus, HumanReadablePlaceOrderPayload } from './abacus';
|
||||
@@ -40,7 +40,7 @@ export type AnalyticsUserPropertyValue<T extends AnalyticsUserProperty> =
|
||||
: T extends AnalyticsUserProperty.WalletConnectionType
|
||||
? WalletConnectionType | undefined
|
||||
: T extends AnalyticsUserProperty.WalletAddress
|
||||
? EthereumAddress | DydxAddress | undefined
|
||||
? EvmAddress | DydxAddress | undefined
|
||||
: // Account
|
||||
T extends AnalyticsUserProperty.DydxAddress
|
||||
? DydxAddress | undefined
|
||||
|
||||
@@ -12,6 +12,7 @@ export enum LocalStorageKey {
|
||||
NotificationsLastUpdated = 'dydx.NotificationsLastUpdated',
|
||||
PushNotificationsEnabled = 'dydx.PushNotificationsEnabled',
|
||||
PushNotificationsLastUpdated = 'dydx.PushNotificationsLastUpdated',
|
||||
TransferNotifications = 'dydx.TransferNotifications',
|
||||
|
||||
// UI State
|
||||
LastViewedMarket = 'dydx.LastViewedMarket',
|
||||
|
||||
@@ -34,6 +34,8 @@ 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: {
|
||||
|
||||
@@ -1,37 +1,49 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import type { StatusResponse } from '@0xsquid/sdk';
|
||||
|
||||
/** implemented in useNotificationTypes */
|
||||
export enum NotificationType {
|
||||
OrderStatusChanged = 'OrderStatusChanged',
|
||||
SquidTransfer = 'SquidTransfer',
|
||||
}
|
||||
|
||||
export enum NotificationComponentType {}
|
||||
|
||||
export type NotificationId = string | number
|
||||
export type NotificationId = string | number;
|
||||
|
||||
export type NotificationTypeConfig<_NotificationId extends NotificationId = string, NotificationUpdateKey = any> = {
|
||||
export type NotificationTypeConfig<
|
||||
_NotificationId extends NotificationId = string,
|
||||
NotificationUpdateKey = any
|
||||
> = {
|
||||
type: NotificationType;
|
||||
|
||||
/** React hook to trigger notifications based on app state */
|
||||
/** React hook to trigger notifications based on app state */
|
||||
useTrigger: (_: {
|
||||
trigger: (
|
||||
trigger: ({
|
||||
id,
|
||||
displayData,
|
||||
updateKey,
|
||||
isNew,
|
||||
}: {
|
||||
/** 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;
|
||||
@@ -58,7 +70,10 @@ export enum NotificationStatus {
|
||||
}
|
||||
|
||||
/** Notification state. Serialized and cached into localStorage. */
|
||||
export type Notification<_NotificationId extends NotificationId = string, NotificationUpdateKey = any> = {
|
||||
export type Notification<
|
||||
_NotificationId extends NotificationId = string,
|
||||
NotificationUpdateKey = any
|
||||
> = {
|
||||
id: _NotificationId;
|
||||
type: NotificationType;
|
||||
status: NotificationStatus;
|
||||
@@ -70,11 +85,15 @@ export type Notifications = Record<NotificationId, Notification<any>>;
|
||||
|
||||
/** Notification display data derived from app state at runtime. */
|
||||
export type NotificationDisplayData = {
|
||||
icon?: React.ReactNode;
|
||||
icon?: React.ReactElement<any, 'svg'>;
|
||||
|
||||
title?: string;
|
||||
|
||||
description?: string;
|
||||
description?: ReactNode;
|
||||
|
||||
customContent?: ReactNode;
|
||||
|
||||
customMenuContent?: ReactNode;
|
||||
|
||||
actionDescription?: string;
|
||||
|
||||
@@ -101,3 +120,13 @@ export type NotificationDisplayData = {
|
||||
*/
|
||||
toastDuration?: number;
|
||||
};
|
||||
|
||||
// Notification types
|
||||
export type TransferNotifcation = {
|
||||
txHash: string;
|
||||
toChainId?: string;
|
||||
fromChainId?: string;
|
||||
toAmount?: number;
|
||||
triggeredAt?: number;
|
||||
status?: StatusResponse;
|
||||
};
|
||||
|
||||
@@ -337,7 +337,7 @@ export const SIGN_TYPED_DATA = {
|
||||
|
||||
export type PrivateInformation = ReturnType<typeof onboarding.deriveHDKeyFromEthereumSignature>;
|
||||
|
||||
export type EthereumAddress = `0x${string}`;
|
||||
export type EvmAddress = `0x${string}`;
|
||||
export type DydxAddress = `dydx${string}`;
|
||||
|
||||
export const DYDX_CHAIN_INFO: Parameters<typeof suggestChain>[0] = {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { formatUnits } from 'viem';
|
||||
|
||||
import { CLIENT_NETWORK_CONFIGS } from '@/constants/networks';
|
||||
import { QUANTUM_MULTIPLIER } from '@/constants/numbers';
|
||||
import { EthereumAddress } from '@/constants/wallets';
|
||||
import { EvmAddress } from '@/constants/wallets';
|
||||
|
||||
import { convertBech32Address } from '@/lib/addressUtils';
|
||||
import { MustBigNumber } from '@/lib/numbers';
|
||||
@@ -59,7 +59,7 @@ export const useAccountBalance = ({
|
||||
token:
|
||||
addressOrDenom === CHAIN_DEFAULT_TOKEN_ADDRESS
|
||||
? undefined
|
||||
: (addressOrDenom as EthereumAddress),
|
||||
: (addressOrDenom as EvmAddress),
|
||||
watch: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { LocalWallet, USDC_DENOM, type Subaccount } from '@dydxprotocol/v4-clien
|
||||
|
||||
import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account';
|
||||
import { LocalStorageKey, LOCAL_STORAGE_VERSIONS } from '@/constants/localStorage';
|
||||
import { DydxAddress, EthereumAddress, PrivateInformation } from '@/constants/wallets';
|
||||
import { DydxAddress, EvmAddress, PrivateInformation } from '@/constants/wallets';
|
||||
|
||||
import {
|
||||
setOnboardingState,
|
||||
@@ -82,7 +82,7 @@ const useAccountsContext = () => {
|
||||
evmAddress,
|
||||
dydxAddress,
|
||||
}: {
|
||||
evmAddress: EthereumAddress;
|
||||
evmAddress: EvmAddress;
|
||||
dydxAddress?: DydxAddress;
|
||||
}) => {
|
||||
saveEvmDerivedAddresses({
|
||||
@@ -192,7 +192,7 @@ const useAccountsContext = () => {
|
||||
} catch (error) {
|
||||
log('useAccounts/setLocalDydxWallet', error);
|
||||
}
|
||||
} else if (evmAddress && signerWagmi) {
|
||||
} else if (evmAddress) {
|
||||
if (!localDydxWallet) {
|
||||
dispatch(setOnboardingState(OnboardingState.WalletConnected));
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { createContext, useContext, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import type { StatusResponse } from '@0xsquid/sdk';
|
||||
|
||||
import { LocalStorageKey } from '@/constants/localStorage';
|
||||
import { type TransferNotifcation } from '@/constants/notifications';
|
||||
|
||||
import { useAccounts } from '@/hooks/useAccounts';
|
||||
import { useSquid } from '@/hooks/useSquid';
|
||||
import { useLocalStorage } from './useLocalStorage';
|
||||
|
||||
const LocalNotificationsContext = createContext<
|
||||
ReturnType<typeof useLocalNotificationsContext> | undefined
|
||||
>(undefined);
|
||||
|
||||
LocalNotificationsContext.displayName = 'LocalNotifications';
|
||||
|
||||
export const LocalNotificationsProvider = ({ ...props }) => (
|
||||
<LocalNotificationsContext.Provider value={useLocalNotificationsContext()} {...props} />
|
||||
);
|
||||
|
||||
export const useLocalNotifications = () => useContext(LocalNotificationsContext)!;
|
||||
|
||||
const TRANSFER_STATUS_FETCH_INTERVAL = 10_000;
|
||||
|
||||
const useLocalNotificationsContext = () => {
|
||||
// transfer notifications
|
||||
const [allTransferNotifications, setAllTransferNotifications] = useLocalStorage<{
|
||||
[key: `dydx${string}`]: TransferNotifcation[];
|
||||
}>({
|
||||
key: LocalStorageKey.TransferNotifications,
|
||||
defaultValue: {},
|
||||
});
|
||||
|
||||
const { dydxAddress } = useAccounts();
|
||||
|
||||
const transferNotifications = dydxAddress ? allTransferNotifications[dydxAddress] || [] : [];
|
||||
|
||||
const setTransferNotifications = useCallback(
|
||||
(notifications: TransferNotifcation[]) => {
|
||||
if (!dydxAddress) return;
|
||||
const updatedNotifications = { ...allTransferNotifications };
|
||||
updatedNotifications[dydxAddress] = notifications;
|
||||
setAllTransferNotifications(updatedNotifications);
|
||||
},
|
||||
[setAllTransferNotifications, dydxAddress]
|
||||
);
|
||||
|
||||
const addTransferNotification = useCallback(
|
||||
(notification: TransferNotifcation) =>
|
||||
setTransferNotifications([...transferNotifications, notification]),
|
||||
[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) {
|
||||
try {
|
||||
if (currentStatus && currentStatus?.squidTransactionStatus !== 'ongoing') continue;
|
||||
|
||||
const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId });
|
||||
if (status) statuses[txHash] = status;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
return statuses;
|
||||
},
|
||||
refetchInterval: TRANSFER_STATUS_FETCH_INTERVAL,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!transferStatuses) return;
|
||||
const newTransferNotifications = transferNotifications.map((notification) => {
|
||||
const status = transferStatuses[notification.txHash];
|
||||
if (!status) return notification;
|
||||
return {
|
||||
...notification,
|
||||
status,
|
||||
};
|
||||
});
|
||||
setTransferNotifications(newTransferNotifications);
|
||||
}, [transferStatuses]);
|
||||
|
||||
return {
|
||||
transferNotifications,
|
||||
addTransferNotification,
|
||||
};
|
||||
};
|
||||
@@ -1,18 +1,26 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
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, TRADE_TYPE_STRINGS, TradeTypes } from '@/constants/trade';
|
||||
import { ORDER_SIDE_STRINGS } 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';
|
||||
|
||||
export const notificationTypes = [
|
||||
@@ -50,34 +58,33 @@ export const notificationTypes = [
|
||||
}
|
||||
: 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) > 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
|
||||
);
|
||||
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,
|
||||
});
|
||||
}
|
||||
}, [orderIds]);
|
||||
}, [orderIds, stringGetter]);
|
||||
},
|
||||
|
||||
useNotificationAction: () => {
|
||||
@@ -93,4 +100,51 @@ export const notificationTypes = [
|
||||
};
|
||||
},
|
||||
} as NotificationTypeConfig<string, [string, number]>,
|
||||
{
|
||||
type: NotificationType.SquidTransfer,
|
||||
useTrigger: ({ trigger }) => {
|
||||
const stringGetter = useStringGetter();
|
||||
const { transferNotifications } = useLocalNotifications();
|
||||
|
||||
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({
|
||||
id: txHash,
|
||||
displayData: {
|
||||
icon: <Icon iconName={finished ? IconName.Transfer : IconName.Clock} />,
|
||||
title: stringGetter({ key: getTitleStringKey(type, finished) }),
|
||||
// TODO: confirm with design what the description should be
|
||||
description: (
|
||||
<>
|
||||
<span>{type === 'deposit' ? 'Deposit of ' : 'Withdraw of'}</span>
|
||||
<Output type={OutputType.Fiat} value={toAmount} />
|
||||
</>
|
||||
),
|
||||
customContent: (
|
||||
<TransferStatusToast
|
||||
toAmount={transfer.toAmount}
|
||||
triggeredAt={transfer.triggeredAt}
|
||||
status={transfer.status}
|
||||
/>
|
||||
),
|
||||
customMenuContent: !finished && <TransferStatusSteps status={transfer.status} />,
|
||||
toastSensitivity: 'foreground',
|
||||
},
|
||||
updateKey: [],
|
||||
});
|
||||
}
|
||||
}, [transferNotifications, stringGetter]);
|
||||
},
|
||||
} as NotificationTypeConfig<string, []>,
|
||||
] satisfies NotificationTypeConfig[];
|
||||
|
||||
@@ -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];
|
||||
@@ -106,7 +106,10 @@ const useNotificationsContext = () => {
|
||||
updateKey,
|
||||
} as Notification);
|
||||
|
||||
updateStatus(notification, isNew ? NotificationStatus.Triggered : NotificationStatus.Cleared);
|
||||
updateStatus(
|
||||
notification,
|
||||
isNew ? NotificationStatus.Triggered : NotificationStatus.Cleared
|
||||
);
|
||||
}
|
||||
|
||||
// updateKey changed - update existing notification
|
||||
@@ -141,7 +144,9 @@ const useNotificationsContext = () => {
|
||||
// Push notifications
|
||||
const [hasEnabledPush, setHasEnabledPush] = useLocalStorage({
|
||||
key: LocalStorageKey.PushNotificationsEnabled,
|
||||
defaultValue: Boolean(globalThis.Notification && globalThis.Notification.permission === 'granted'),
|
||||
defaultValue: Boolean(
|
||||
globalThis.Notification && globalThis.Notification.permission === 'granted'
|
||||
),
|
||||
});
|
||||
const [isEnablingPush, setIsEnablingPush] = useState(false);
|
||||
|
||||
@@ -177,28 +182,23 @@ const useNotificationsContext = () => {
|
||||
const iconUrl =
|
||||
displayData.icon && (await renderSvgToDataUrl(displayData.icon).catch(() => undefined));
|
||||
|
||||
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),
|
||||
});
|
||||
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,
|
||||
});
|
||||
|
||||
pushNotification.addEventListener('click', () => {
|
||||
onNotificationAction(notification);
|
||||
markSeen(notification);
|
||||
});
|
||||
pushNotification.addEventListener('click', () => {
|
||||
onNotificationAction(notification);
|
||||
markSeen(notification);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setPushNotificationsLastUpdated(Date.now());
|
||||
@@ -236,4 +236,3 @@ const useNotificationsContext = () => {
|
||||
setIsMenuOpen,
|
||||
};
|
||||
};
|
||||
// }
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
|
||||
import { Squid } from '@0xsquid/sdk';
|
||||
|
||||
import { DydxV4Network, isDydxV4Network } from '@/constants/networks';
|
||||
import { CLIENT_NETWORK_CONFIGS, DydxV4Network, isDydxV4Network } from '@/constants/networks';
|
||||
|
||||
import { getSelectedNetwork } from '@/state/appSelectors';
|
||||
|
||||
const SQUID_BASE_URL: Record<DydxV4Network, string | undefined> = {
|
||||
[DydxV4Network.V4Testnet2]: 'https://squid-api-git-feat-cosmos-maintestnet-0xsquid.vercel.app',
|
||||
[DydxV4Network.V4Staging]: undefined,
|
||||
[DydxV4Network.V4Local]: undefined,
|
||||
[DydxV4Network.V4Mainnet]: 'https://api.0xsquid.com',
|
||||
};
|
||||
export const NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
|
||||
|
||||
const useSquidContext = () => {
|
||||
const selectedNetwork = useSelector(getSelectedNetwork);
|
||||
@@ -27,7 +23,7 @@ const useSquidContext = () => {
|
||||
const squid = useMemo(
|
||||
() =>
|
||||
isDydxV4Network(selectedNetwork)
|
||||
? new Squid({ baseUrl: SQUID_BASE_URL[selectedNetwork as DydxV4Network] })
|
||||
? new Squid({ baseUrl: CLIENT_NETWORK_CONFIGS[selectedNetwork]?.endpoints['0xsquid'] })
|
||||
: undefined,
|
||||
[selectedNetwork]
|
||||
);
|
||||
@@ -4,7 +4,7 @@ import { LocalStorageKey } from '@/constants/localStorage';
|
||||
|
||||
import {
|
||||
type DydxAddress,
|
||||
type EthereumAddress,
|
||||
type EvmAddress,
|
||||
WalletConnectionType,
|
||||
WalletType,
|
||||
wallets,
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
useConnect as useConnectWagmi,
|
||||
useAccount as useAccountWagmi,
|
||||
useDisconnect as useDisconnectWagmi,
|
||||
usePublicClient as usePublicClientWagmi,
|
||||
useWalletClient as useWalletClientWagmi,
|
||||
} from 'wagmi';
|
||||
import {
|
||||
@@ -37,11 +38,12 @@ export const useWalletConnection = () => {
|
||||
const stringGetter = useStringGetter();
|
||||
|
||||
// EVM wallet connection
|
||||
const [evmAddress, saveEvmAddress] = useLocalStorage<EthereumAddress | undefined>({
|
||||
const [evmAddress, saveEvmAddress] = useLocalStorage<EvmAddress | undefined>({
|
||||
key: LocalStorageKey.EvmAddress,
|
||||
defaultValue: undefined,
|
||||
});
|
||||
const { address: evmAddressWagmi, isConnected: isConnectedWagmi } = useAccountWagmi();
|
||||
const publicClientWagmi = usePublicClientWagmi();
|
||||
const { data: signerWagmi } = useWalletClientWagmi();
|
||||
const { disconnectAsync: disconnectWagmi } = useDisconnectWagmi();
|
||||
|
||||
@@ -214,7 +216,8 @@ export const useWalletConnection = () => {
|
||||
evmAddress,
|
||||
evmAddressWagmi,
|
||||
signerWagmi,
|
||||
|
||||
publicClientWagmi,
|
||||
|
||||
// Wallet connection (Cosmos)
|
||||
dydxAddress,
|
||||
dydxAddressGraz,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -36,6 +36,7 @@ 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 {
|
||||
@@ -59,7 +60,7 @@ class AbacusStateManager {
|
||||
new AbacusRest(),
|
||||
this.websocket,
|
||||
new AbacusChainTransaction(),
|
||||
null,
|
||||
new AbacusTracking(),
|
||||
new AbacusThreading(),
|
||||
new CoroutineTimer(),
|
||||
new AbacusFileSystem()
|
||||
@@ -153,6 +154,7 @@ class AbacusStateManager {
|
||||
|
||||
// ------ Set Data ------ //
|
||||
setStore = (store: RootStore) => {
|
||||
this.store = store;
|
||||
this.stateNotifier.setStore(store);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { AbacusTrackingProtocol, Nullable } from '@/constants/abacus';
|
||||
|
||||
class AbacusTracking implements AbacusTrackingProtocol {
|
||||
log(event: string, data: Nullable<string>) {
|
||||
console.log({ event, data });
|
||||
}
|
||||
}
|
||||
|
||||
export default AbacusTracking;
|
||||
@@ -21,8 +21,10 @@ 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) => resolve(e.target.result),
|
||||
onerror: (e) => reject(e.target.error),
|
||||
onload: (e: {
|
||||
target: { result: string | ArrayBuffer | PromiseLike<string | ArrayBuffer | null> | null };
|
||||
}) => resolve(e.target.result),
|
||||
onerror: (e: { target: { error: Error } }) => reject(e.target.error),
|
||||
}).readAsDataURL(new File([bytes], '', { type }));
|
||||
});
|
||||
|
||||
|
||||
@@ -72,3 +72,9 @@ export const getTimeTillNextUnit = (unit: 'minute' | 'hour' | 'day') => {
|
||||
};
|
||||
|
||||
export const getTimeString = (time: number) => time.toString().padStart(2, '0');
|
||||
|
||||
export const formatSeconds = (seconds: number) => {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${getTimeString(minutes)}:${getTimeString(remainingSeconds)}`;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useInterval } from '@/hooks';
|
||||
|
||||
import { getTimeTillNextUnit, getTimeString } from '@/lib/timeUtils';
|
||||
import { getTimeTillNextUnit, formatSeconds } from '@/lib/timeUtils';
|
||||
import { Output, OutputType } from '@/components/Output';
|
||||
|
||||
export const NextFundingTimer = () => {
|
||||
@@ -18,7 +18,7 @@ export const NextFundingTimer = () => {
|
||||
type={OutputType.Text}
|
||||
value={
|
||||
secondsLeft !== undefined
|
||||
? `${getTimeString(Math.floor(secondsLeft / 60))}:${getTimeString(secondsLeft % 60)}`
|
||||
? formatSeconds(secondsLeft)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { useCallback, useState, useMemo } from 'react';
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
|
||||
import { Root, Trigger, Content } from '@radix-ui/react-collapsible';
|
||||
import { StatusResponse } from '@0xsquid/sdk';
|
||||
|
||||
import { useInterval, useStringGetter } from '@/hooks';
|
||||
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
|
||||
import { formatSeconds } from '@/lib/timeUtils';
|
||||
|
||||
import { Output, OutputType } from '@/components/Output';
|
||||
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';
|
||||
|
||||
type ElementProps = {
|
||||
toAmount?: number;
|
||||
triggeredAt?: number;
|
||||
status?: StatusResponse;
|
||||
};
|
||||
|
||||
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 type = useMemo(
|
||||
() => (status?.toChain?.chainData?.chainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdrawal'),
|
||||
[status]
|
||||
);
|
||||
|
||||
const updateSecondsLeft = useCallback(() => {
|
||||
const fromChainEta = (status?.fromChain?.chainData?.estimatedRouteDuration || 0) * 1000;
|
||||
const toChainEta = (status?.toChain?.chainData?.estimatedRouteDuration || 0) * 1000;
|
||||
setSecondsLeft(Math.floor((triggeredAt + fromChainEta + toChainEta - Date.now()) / 1000));
|
||||
}, [status]);
|
||||
|
||||
useInterval({ callback: updateSecondsLeft });
|
||||
|
||||
if (!status) return <LoadingDots size={3} />;
|
||||
|
||||
return (
|
||||
<Styled.Root open={open} onOpenChange={setOpen}>
|
||||
<WithReceipt
|
||||
hideReceipt={!open}
|
||||
side="bottom"
|
||||
slotReceipt={
|
||||
<Styled.Receipt>
|
||||
<TransferStatusSteps status={status} />
|
||||
</Styled.Receipt>
|
||||
}
|
||||
>
|
||||
<Styled.BridgingStatus>
|
||||
<Styled.Status>
|
||||
{stringGetter({
|
||||
key: type === 'deposit' ? STRING_KEYS.DEPOSIT_STATUS : STRING_KEYS.WITHDRAW_STATUS,
|
||||
params: {
|
||||
AMOUNT_USD: <Styled.InlineOutput type={OutputType.Fiat} value={toAmount} />,
|
||||
ESTIMATED_DURATION: (
|
||||
<Styled.InlineOutput
|
||||
type={OutputType.Text}
|
||||
value={formatSeconds(Math.max(secondsLeft || 0, 0))}
|
||||
/>
|
||||
),
|
||||
},
|
||||
})}
|
||||
</Styled.Status>
|
||||
<Styled.Trigger>
|
||||
<Styled.TriggerIcon>
|
||||
<Icon iconName={IconName.Caret} />
|
||||
</Styled.TriggerIcon>
|
||||
{stringGetter({ key: open ? STRING_KEYS.HIDE_DETAILS : STRING_KEYS.VIEW_DETAILS })}
|
||||
</Styled.Trigger>
|
||||
</Styled.BridgingStatus>
|
||||
</WithReceipt>
|
||||
</Styled.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.Root = styled(Root)`
|
||||
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`
|
||||
${layoutMixins.flexColumn};
|
||||
background-color: var(--color-layer-3);
|
||||
|
||||
gap: 0.5rem;
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
`;
|
||||
|
||||
Styled.Status = styled.div`
|
||||
color: var(--color-text-0);
|
||||
|
||||
font-size: 0.875rem;
|
||||
`;
|
||||
|
||||
Styled.InlineOutput = styled(Output)`
|
||||
display: inline-block;
|
||||
|
||||
color: var(--color-text-1);
|
||||
`;
|
||||
|
||||
Styled.Content = styled(Content)`
|
||||
${layoutMixins.flexColumn};
|
||||
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
`;
|
||||
|
||||
Styled.Step = styled.div`
|
||||
${layoutMixins.row};
|
||||
|
||||
gap: 0.5rem;
|
||||
`;
|
||||
|
||||
Styled.Trigger = styled(Trigger)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
|
||||
color: var(--color-accent);
|
||||
user-select: none;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.TriggerIcon = styled.span`
|
||||
width: 0.75em;
|
||||
|
||||
display: inline-flex;
|
||||
transition: transform 0.3s var(--ease-out-expo);
|
||||
|
||||
${Styled.Trigger}[data-state='open'] & {
|
||||
rotate: -0.5turn;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,175 @@
|
||||
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-js';
|
||||
|
||||
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 { currentStep, steps, type } = useMemo(() => {
|
||||
const routeStatus = status?.routeStatus;
|
||||
const fromChain = status?.fromChain?.chainData?.chainId;
|
||||
const toChain = status?.toChain?.chainData?.chainId;
|
||||
const type = toChain === TESTNET_CHAIN_ID ? 'deposit' : 'withdrawal';
|
||||
|
||||
const steps = [
|
||||
{
|
||||
label: stringGetter({
|
||||
key:
|
||||
type === 'deposit' ? STRING_KEYS.INITIATED_DEPOSIT : STRING_KEYS.INITIATED_WITHDRAWAL,
|
||||
}),
|
||||
step: TransferStatusStep.FromChain,
|
||||
link: status?.fromChain?.transactionUrl,
|
||||
},
|
||||
{
|
||||
label: stringGetter({ key: STRING_KEYS.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,
|
||||
},
|
||||
];
|
||||
|
||||
const currentStatus = routeStatus[routeStatus?.length - 1];
|
||||
|
||||
let currentStep = TransferStatusStep.Bridge;
|
||||
|
||||
if (!routeStatus?.length) {
|
||||
currentStep = TransferStatusStep.FromChain;
|
||||
} else if (currentStatus.chainId === toChain) {
|
||||
currentStep =
|
||||
currentStatus.status !== 'success'
|
||||
? TransferStatusStep.ToChain
|
||||
: TransferStatusStep.Complete;
|
||||
} else if (currentStatus.chainId === fromChain && currentStatus.status !== 'success') {
|
||||
currentStep = TransferStatusStep.FromChain;
|
||||
}
|
||||
|
||||
return {
|
||||
currentStep,
|
||||
steps,
|
||||
type,
|
||||
};
|
||||
}, [status, stringGetter]);
|
||||
|
||||
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);
|
||||
`}
|
||||
`;
|
||||
@@ -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 } from '@/constants/localization';
|
||||
import { STRING_KEYS, StringKey } from '@/constants/localization';
|
||||
|
||||
import { AssetIcon } from '@/components/AssetIcon';
|
||||
import { Button } from '@/components/Button';
|
||||
@@ -38,6 +38,9 @@ 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();
|
||||
@@ -47,7 +50,7 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
|
||||
const { cancelOrder } = useSubaccount();
|
||||
|
||||
const {
|
||||
asset = {},
|
||||
asset,
|
||||
cancelReason,
|
||||
createdAtMilliseconds,
|
||||
expiresAtMilliseconds,
|
||||
@@ -57,7 +60,7 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
|
||||
price,
|
||||
reduceOnly,
|
||||
totalFilled,
|
||||
resources = {},
|
||||
resources,
|
||||
size,
|
||||
status,
|
||||
stepSizeDecimals,
|
||||
@@ -109,9 +112,9 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
|
||||
<Styled.Row>
|
||||
<Styled.StatusIcon iconName={statusIcon} color={statusIconColor} />
|
||||
<Styled.Status>
|
||||
{statusStringKey
|
||||
{isStringKey(statusStringKey)
|
||||
? stringGetter({ key: statusStringKey })
|
||||
: resources.statusStringKey
|
||||
: isStringKey(resources.statusStringKey)
|
||||
? stringGetter({ key: resources.statusStringKey })
|
||||
: undefined}
|
||||
</Styled.Status>
|
||||
@@ -121,7 +124,9 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
|
||||
{
|
||||
key: 'cancel-reason',
|
||||
label: stringGetter({ key: STRING_KEYS.CANCEL_REASON }),
|
||||
value: cancelReason ? stringGetter({ key: STRING_KEYS[cancelReason] }) : undefined,
|
||||
value: isStringKey(cancelReason)
|
||||
? stringGetter({ key: STRING_KEYS[cancelReason] })
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
key: 'amount',
|
||||
|
||||
@@ -3,16 +3,21 @@ import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
import { type NumberFormatValues } from 'react-number-format';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
|
||||
import { ethers } from 'ethers';
|
||||
import { parseUnits } from 'viem'
|
||||
|
||||
import erc20 from '@/abi/erc20.json';
|
||||
import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus';
|
||||
import { AlertType } from '@/constants/alerts';
|
||||
import { ButtonSize } from '@/constants/buttons';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { NumberSign } from '@/constants/numbers';
|
||||
import type { EvmAddress } from '@/constants/wallets';
|
||||
|
||||
import { useAccounts, useDebounce, useStringGetter } from '@/hooks';
|
||||
import { useAccountBalance } from '@/hooks/useAccountBalance';
|
||||
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
|
||||
import { NATIVE_TOKEN_ADDRESS, useSquid } from '@/hooks/useSquid';
|
||||
import { useWalletConnection } from '@/hooks/useWalletConnection';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
import { formMixins } from '@/styles/formMixins';
|
||||
@@ -32,6 +37,7 @@ import { getTransferInputs } from '@/state/inputsSelectors';
|
||||
|
||||
import abacusStateManager from '@/lib/abacus';
|
||||
import { MustBigNumber } from '@/lib/numbers';
|
||||
import { log } from '@/lib/telemetry';
|
||||
|
||||
import { ChainSelectMenu } from './ChainSelectMenu';
|
||||
import { TokenSelectMenu } from './TokenSelectMenu';
|
||||
@@ -48,13 +54,17 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { signerWagmi } = useAccounts();
|
||||
const { evmAddress, signerWagmi } = useAccounts();
|
||||
const { publicClientWagmi } = useWalletConnection();
|
||||
|
||||
const { addTransferNotification } = useLocalNotifications();
|
||||
|
||||
const {
|
||||
requestPayload,
|
||||
token,
|
||||
chain: chainIdStr,
|
||||
resources,
|
||||
summary,
|
||||
} = useSelector(getTransferInputs, shallowEqual) || {};
|
||||
const chainId = chainIdStr ? parseInt(chainIdStr) : undefined;
|
||||
|
||||
@@ -64,13 +74,16 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
|
||||
[token, resources]
|
||||
);
|
||||
|
||||
const sourceChain = useMemo(
|
||||
() => (chainIdStr ? resources?.chainResources?.get(chainIdStr) : undefined),
|
||||
[chainId, resources]
|
||||
);
|
||||
|
||||
const [fromAmount, setFromAmount] = useState('');
|
||||
const [slippage, setSlippage] = useState(0.01); // 1% slippage
|
||||
const debouncedAmount = useDebounce<string>(fromAmount, 500);
|
||||
|
||||
// Async Data
|
||||
const [transactionHash, setTransactionHash] = useState<string>();
|
||||
|
||||
const { balance, queryStatus, isQueryFetching } = useAccountBalance({
|
||||
addressOrDenom: sourceToken?.address || undefined,
|
||||
assetSymbol: sourceToken?.symbol || undefined,
|
||||
@@ -157,6 +170,39 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
|
||||
}
|
||||
}, [balance, setFromAmount]);
|
||||
|
||||
const validateTokenApproval = useCallback(async () => {
|
||||
if (!signerWagmi || !publicClientWagmi) throw new Error('Missing signer');
|
||||
if (!sourceToken?.address || !sourceToken.decimals) throw new Error('Missing source token address');
|
||||
if (!sourceChain?.rpc) throw new Error('Missing source chain rpc');
|
||||
if (!requestPayload?.targetAddress) throw new Error('Missing target address');
|
||||
if (!requestPayload?.value) throw new Error('Missing transaction value');
|
||||
if (sourceToken?.address === NATIVE_TOKEN_ADDRESS) return;
|
||||
|
||||
const allowance = await publicClientWagmi.readContract({
|
||||
address: sourceToken.address as EvmAddress,
|
||||
abi: erc20,
|
||||
functionName: 'allowance',
|
||||
args: [evmAddress as EvmAddress, requestPayload.targetAddress as EvmAddress]
|
||||
});
|
||||
|
||||
const sourceAmountBN = parseUnits(debouncedAmount, sourceToken.decimals);
|
||||
|
||||
if (sourceAmountBN > (allowance as bigint)) {
|
||||
const { request } = await publicClientWagmi.simulateContract({
|
||||
account: evmAddress,
|
||||
address: sourceToken.address as EvmAddress,
|
||||
abi: erc20,
|
||||
functionName: 'approve',
|
||||
args: [requestPayload.targetAddress as EvmAddress, sourceAmountBN],
|
||||
})
|
||||
|
||||
const approveTx = await signerWagmi.writeContract(request);
|
||||
await publicClientWagmi.waitForTransactionReceipt({
|
||||
hash: approveTx,
|
||||
})
|
||||
}
|
||||
}, [signerWagmi, sourceToken, sourceChain, requestPayload, publicClientWagmi]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (e: FormEvent) => {
|
||||
try {
|
||||
@@ -178,27 +224,32 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
await validateTokenApproval();
|
||||
|
||||
let tx = {
|
||||
to: requestPayload.targetAddress as `0x${string}`,
|
||||
data: requestPayload.data as `0x${string}`,
|
||||
gasLimit: ethers.toBigInt(requestPayload.gasLimit),
|
||||
to: requestPayload.targetAddress as EvmAddress,
|
||||
data: requestPayload.data as EvmAddress,
|
||||
gasLimit: BigInt(requestPayload.gasLimit),
|
||||
value:
|
||||
requestPayload.routeType !== 'SEND' ? ethers.toBigInt(requestPayload.value) : undefined,
|
||||
requestPayload.routeType !== 'SEND' ? BigInt(requestPayload.value) : undefined,
|
||||
};
|
||||
const txHash = await signerWagmi.sendTransaction(tx);
|
||||
|
||||
onDeposit?.();
|
||||
|
||||
if (txHash) {
|
||||
setTransactionHash(txHash);
|
||||
abacusStateManager.setTransferStatus({
|
||||
hash: txHash,
|
||||
addTransferNotification({
|
||||
txHash: txHash,
|
||||
toChainId: TESTNET_CHAIN_ID,
|
||||
fromChainId: chainId?.toString(),
|
||||
fromChainId: chainIdStr || undefined,
|
||||
toAmount: summary?.usdcSize || undefined,
|
||||
triggeredAt: Date.now(),
|
||||
});
|
||||
abacusStateManager.clearTransferInputValues();
|
||||
setFromAmount('');
|
||||
}
|
||||
} catch (error) {
|
||||
log('DepositForm/onSubmit', error);
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -286,17 +337,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}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { STRING_KEYS } from '@/constants/localization';
|
||||
import { NumberSign, QUANTUM_MULTIPLIER } from '@/constants/numbers';
|
||||
|
||||
import { useDebounce, useStringGetter, useSubaccount } from '@/hooks';
|
||||
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
import { formMixins } from '@/styles/formMixins';
|
||||
@@ -63,9 +64,9 @@ export const WithdrawForm = () => {
|
||||
[token, resources]
|
||||
);
|
||||
|
||||
// Async Data
|
||||
const [transactionHash, setTransactionHash] = useState<string>();
|
||||
const { addTransferNotification } = useLocalNotifications();
|
||||
|
||||
// Async Data
|
||||
const debouncedAmountBN = MustBigNumber(debouncedAmount);
|
||||
const withdrawAmountBN = MustBigNumber(withdrawAmount);
|
||||
const freeCollateralBN = MustBigNumber(freeCollateral?.current);
|
||||
@@ -133,11 +134,12 @@ export const WithdrawForm = () => {
|
||||
if (txHash?.hash) {
|
||||
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(),
|
||||
triggeredAt: Date.now(),
|
||||
});
|
||||
abacusStateManager.clearTransferInputValues();
|
||||
setWithdrawAmount('');
|
||||
@@ -148,7 +150,7 @@ export const WithdrawForm = () => {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[setTransactionHash, requestPayload, debouncedAmountBN, chainIdStr]
|
||||
[requestPayload, debouncedAmountBN, chainIdStr]
|
||||
);
|
||||
|
||||
const onChangeAddress = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -291,17 +293,7 @@ export const WithdrawForm = () => {
|
||||
}
|
||||
/>
|
||||
</Styled.WithDetailsReceipt>
|
||||
{errorMessage ? (
|
||||
<AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>
|
||||
) : (
|
||||
transactionHash && (
|
||||
<AlertMessage type={AlertType.Success}>
|
||||
<Styled.TransactionInfo>
|
||||
{stringGetter({ key: STRING_KEYS.WITHDRAW_IN_PROGRESS })}
|
||||
</Styled.TransactionInfo>
|
||||
</AlertMessage>
|
||||
)
|
||||
)}
|
||||
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>}
|
||||
<WithdrawButtonAndReceipt
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { groupBy } from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
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';
|
||||
@@ -13,9 +15,7 @@ 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,6 +27,8 @@ export const NotificationsMenu = ({
|
||||
slotTrigger,
|
||||
placement = DialogPlacement.Sidebar,
|
||||
}: ElementProps) => {
|
||||
const stringGetter = useStringGetter();
|
||||
|
||||
const {
|
||||
notifications,
|
||||
getDisplayData,
|
||||
@@ -69,7 +71,7 @@ export const NotificationsMenu = ({
|
||||
.map(([status, notifications]) => ({
|
||||
group: status,
|
||||
groupLabel: {
|
||||
[NotificationStatus.Triggered]: 'New',
|
||||
[NotificationStatus.Triggered]: stringGetter({ key: STRING_KEYS.NEW }),
|
||||
// [NotificationStatus.Updated]: 'Updates',
|
||||
[NotificationStatus.Seen]: 'Seen',
|
||||
[NotificationStatus.Cleared]: 'Archived',
|
||||
@@ -89,9 +91,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}
|
||||
@@ -135,7 +137,7 @@ export const NotificationsMenu = ({
|
||||
isOpen={isMenuOpen || placement === DialogPlacement.Inline}
|
||||
setIsOpen={setIsMenuOpen}
|
||||
items={items}
|
||||
title="Notifications"
|
||||
title={stringGetter({ key: STRING_KEYS.NOTIFICATIONS })}
|
||||
slotTrigger={
|
||||
<$TriggerContainer>
|
||||
{slotTrigger}
|
||||
@@ -168,7 +170,7 @@ export const NotificationsMenu = ({
|
||||
),
|
||||
}}
|
||||
>
|
||||
Clear All
|
||||
{stringGetter({ key: STRING_KEYS.CLEAR })}
|
||||
</Button>
|
||||
</$FooterToolbar>
|
||||
}
|
||||
|
||||