Compare commits

...
Author SHA1 Message Date
Bill He 234f3f2c93 address comments 2023-12-12 12:58:55 -05:00
Bill He 086bb4d10a bump packages 2023-12-12 10:55:13 -05:00
Bill He ae8631bf2c Implement CCTP withdraw 2023-12-12 10:49:58 -05:00
Jared Vu 4a044da38f Size inputs (#191)
* use formattedValue to update redux state

* Separate updateLeverage and onLeverageInput

* Remove console.log
2023-12-12 07:11:51 -08:00
Jared Vu fbbfbc6580 Bump v4-abacus and v4-localization (#189) 2023-12-08 15:41:07 -08:00
Rui 2714c2b084 Fix path to chainLogo (#187) 2023-12-07 13:03:44 -08:00
Jared Vu 9979bc8a16 Prevent Spread row from moving when orderbook updates (#186)
* Add dummy rows to ensure even row count

* Reverse bids for horizontal layout

* disable pointer-events for dummy rows
2023-12-06 10:26:47 -08:00
aleka a958a59ea2 Display more markets 12/06 (#178)
* add more markets 12/06
2023-12-06 11:38:49 -05:00
Jared Vu 2bf9e66da5 Fix inputs for comma locales (#171)
* Fix for comma decimal separator locales

* Accept tickSizeDecimals 0

* Use FloatValue as value for Size Input

* Use formattedValue to handle prefix/suffix

* Use MustBigNumber.toString()
2023-12-05 16:22:41 -08:00
Bill 2980144e40 Enable CCTP via TestFlags (#185)
* Enable CCTP via Env Var

* Use test flags instead
2023-12-05 14:48:47 -08:00
aleka 5387fe8632 Display more markets 12/05 (#177)
* add more markets 12/05
2023-12-05 09:52:12 -05:00
Bill 1132ea3bbd Do not init nobleClient if nobleValidatorUrl does not exist (#184) 2023-12-04 11:19:29 -08:00
aleka 9188e08a4f prevent 1password fill (#183) 2023-12-04 11:06:49 -05:00
aleka 941ef7f9dd Display more markets 12/04 (#176)
* add more markets 11/30

* add more markets 12/01

* add more markets 12/04
2023-12-04 10:53:25 -05:00
Bill b1ffa2e219 Support CCTP and noble auto deposits on testnet (#180)
* Support CCTP and noble auto deposits on testnet

* slippage

* fix fees

* update isCctp, and comments

* bump packages

* cctp.json

* Bump abacus

* fix error

* fix loading button
2023-12-01 15:18:50 -08:00
36 changed files with 626 additions and 325 deletions
+3 -3
View File
@@ -39,9 +39,9 @@
"@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0",
"@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/v4-abacus": "^1.0.30",
"@dydxprotocol/v4-client-js": "^1.0.0",
"@dydxprotocol/v4-localization": "^1.0.17",
"@dydxprotocol/v4-abacus": "^1.1.22",
"@dydxprotocol/v4-client-js": "^1.0.11",
"@dydxprotocol/v4-localization": "^1.0.18",
"@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3",
"@radix-ui/react-checkbox": "^1.0.4",
+30 -17
View File
@@ -1,5 +1,9 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
'@0xsquid/sdk':
specifier: ^1.10.0
@@ -23,14 +27,14 @@ dependencies:
specifier: ^0.31.0
version: 0.31.0
'@dydxprotocol/v4-abacus':
specifier: ^1.0.30
version: 1.0.30
specifier: ^1.1.22
version: 1.1.22
'@dydxprotocol/v4-client-js':
specifier: ^1.0.0
version: 1.0.0
specifier: ^1.0.11
version: 1.0.11
'@dydxprotocol/v4-localization':
specifier: ^1.0.17
version: 1.0.17
specifier: ^1.0.18
version: 1.0.18
'@ethersproject/providers':
specifier: ^5.7.2
version: 5.7.2
@@ -984,12 +988,12 @@ packages:
resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==}
dev: true
/@dydxprotocol/v4-abacus@1.0.30:
resolution: {integrity: sha512-zzJzNzMR3Stwv1Mu0XGUNlyo1V/ywo3yi3QSzE4mzUtXB+p5Shj5hRQCcwhHcNjLgp6a+T0BcRjBPa4QMETmwg==}
/@dydxprotocol/v4-abacus@1.1.22:
resolution: {integrity: sha512-txinqnKGblCEcn1FjMj5nW9Tv2446fbkRaquodwQSk0GTP92/78xl4VSOwMngc0f4heiVGbirlCKyHmtZUADmA==}
dev: false
/@dydxprotocol/v4-client-js@1.0.0:
resolution: {integrity: sha512-ehfHO+zQy795TcJRwtiawadFHZyh4HnpJNP26hCGsIjLE5q6LLHweQHpK/1N/sXU1PBOuQwc7iaJnFop6gYauQ==}
/@dydxprotocol/v4-client-js@1.0.11:
resolution: {integrity: sha512-rJNFGTO+HU1GQppXeRLKXNk4HWx/h+DjU2sUCieEv5mnRIMQpLw8dH/L+Uyhxx2oovccY1MaKB4Fr9IngdrF0A==}
dependencies:
'@cosmjs/amino': 0.30.1
'@cosmjs/encoding': 0.31.1
@@ -1009,15 +1013,15 @@ packages:
ethers: 6.6.1
long: 4.0.0
protobufjs: 6.11.4
ws: 8.13.0(bufferutil@4.0.7)(utf-8-validate@5.0.10)
ws: 8.15.0
transitivePeerDependencies:
- bufferutil
- debug
- utf-8-validate
dev: false
/@dydxprotocol/v4-localization@1.0.17:
resolution: {integrity: sha512-fybNpi1ono1IdwFQJTszsHbHmGti2BdhTo1nz9KOmKh3tmXdhWObKb64Ve6/6Fjk6h/9+DeZ5ZG4UbYyYMyhCA==}
/@dydxprotocol/v4-localization@1.0.18:
resolution: {integrity: sha512-790ajXfGzqRs7JVA3vISCg0Vh4EEN7SlcI8u04iryPcBp1WwkQ5TFGllF32+YPs1+mgjMRhwrg6jP8zEGMQFyw==}
dev: false
/@dydxprotocol/v4-proto@0.4.1:
@@ -14033,6 +14037,19 @@ packages:
utf-8-validate: 5.0.10
dev: false
/ws@8.15.0:
resolution: {integrity: sha512-H/Z3H55mrcrgjFwI+5jKavgXvwQLtfPCUEp6pi35VhoB0pfcHnSoyuTzkBEZpzq49g1193CUEwIvmsjcotenYw==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
dev: false
/ws@8.5.0:
resolution: {integrity: sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==}
engines: {node: '>=10.0.0'}
@@ -14136,7 +14153,3 @@ packages:
/zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: true
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+3 -3
View File
@@ -1,7 +1,7 @@
[
{
"chainId": "42161",
"tokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"tokenAddress": "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8",
"name": "Arbitrum"
},
{
@@ -11,7 +11,7 @@
},
{
"chainId": "8453",
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"tokenAddress": "0x66627F389ae46D881773B7131139b2411980E09E",
"name": "Base"
},
{
@@ -21,7 +21,7 @@
},
{
"chainId": "10",
"tokenAddress": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
"tokenAddress": "0x7F5c764cBc14f9669B88837ca1490cCa17c31607",
"name": "OP Mainnet"
},
{
+15 -15
View File
@@ -42,7 +42,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -116,7 +116,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -189,7 +189,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -262,7 +262,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -335,7 +335,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -410,7 +410,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -483,7 +483,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -558,7 +558,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -637,7 +637,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -713,7 +713,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -789,7 +789,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -865,7 +865,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -941,7 +941,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -1008,7 +1008,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -1084,7 +1084,7 @@
"ethereumChainId": "1",
"dydxChainId": "[mainnet chain id]",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "[mainnet squid integrator id]",
"isMainNet": true,
"tokens": {
-4
View File
@@ -22,8 +22,6 @@ import { NotificationsProvider } from '@/hooks/useNotifications';
import { LocalNotificationsProvider } from '@/hooks/useLocalNotifications';
import { RestrictionProvider } from '@/hooks/useRestrictions';
import { SubaccountProvider } from '@/hooks/useSubaccount';
import { SquidProvider } from '@/hooks/useSquid';
import { TestFlagsProvider } from '@/hooks/useTestFlags';
import { GuardedMobileRoute } from '@/components/GuardedMobileRoute';
@@ -124,13 +122,11 @@ const providers = [
wrapProvider(QueryClientProvider, { client: queryClient }),
wrapProvider(GrazProvider),
wrapProvider(WagmiConfig, { config }),
wrapProvider(TestFlagsProvider),
wrapProvider(LocaleProvider),
wrapProvider(RestrictionProvider),
wrapProvider(DydxProvider),
wrapProvider(AccountsProvider),
wrapProvider(SubaccountProvider),
wrapProvider(SquidProvider),
wrapProvider(LocalNotificationsProvider),
wrapProvider(NotificationsProvider),
wrapProvider(DialogAreaProvider),
+1 -1
View File
@@ -61,7 +61,7 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
return (
<StyledBaseButton
disabled={state[ButtonState.Disabled]}
disabled={state[ButtonState.Disabled] || state[ButtonState.Loading]}
{...{ ref, action, size, shape, state, ...otherProps }}
>
{
+30 -20
View File
@@ -10,7 +10,7 @@ import {
USD_DECIMALS,
} from '@/constants/numbers';
import { BIG_NUMBERS, MustBigNumber } from '@/lib/numbers';
import { BIG_NUMBERS } from '@/lib/numbers';
import { useLocaleSeparators } from '@/hooks';
export enum InputType {
@@ -29,31 +29,38 @@ type StyleProps = {
type ElementProps = {
type?: InputType;
value?: string | number | null;
allowNegative?: boolean;
decimals?: number;
disabled?: boolean;
id?: string;
max?: number;
onBlur?: () => void;
onChange?:
| Dispatch<SetStateAction<string>>
| React.ReactEventHandler<HTMLInputElement>
| ((values: NumberFormatValues, e: SourceInfo) => void);
onFocus?: () => void;
onInput?: ({
value,
floatValue,
formattedValue,
}: {
value: string;
floatValue?: number;
formattedValue: string;
}) => void;
placeholder?: string;
};
export type InputProps = ElementProps & StyleProps;
type ConditionalProps =
| {
allowNegative?: boolean;
decimals?: number;
max?: number;
onChange?: (values: NumberFormatValues, e: SourceInfo) => void;
onInput?: ({
value,
floatValue,
formattedValue,
}: {
value: string;
floatValue?: number;
formattedValue: string;
}) => void;
}
| {
allowNegative?: never;
decimals?: never;
max?: never;
onChange?: Dispatch<SetStateAction<string>> | React.ReactEventHandler<HTMLInputElement>;
onInput?: (e: SyntheticInputEvent) => void;
};
export type InputProps = ElementProps & StyleProps & ConditionalProps;
export const Input = forwardRef<HTMLInputElement, InputProps>(
(
@@ -129,6 +136,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
placeholder={placeholder}
value={value}
// Other
data-1p-ignore // prevent 1Password fill
{...otherProps}
/>
) : (
@@ -137,6 +145,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
ref={ref as React.Ref<typeof NumericFormat<unknown>>}
id={id}
// NumericFormat
valueIsNumericString
allowNegative={allowNegative}
decimalScale={decimals}
decimalSeparator={LOCALE_DECIMAL_SEPARATOR}
@@ -160,7 +169,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
? undefined
: Number(formattedValue.replace(',', '.'));
onInput?.({ value, floatValue, formattedValue });
onInput?.({ value, floatValue, formattedValue, ...e });
}}
// Native
disabled={disabled}
@@ -168,6 +177,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
value={formattedValue}
autoComplete="off"
autoCorrect="off"
data-1p-ignore // prevent 1Password fill
{...otherProps}
/>
)}
+15
View File
@@ -46,4 +46,19 @@ export const MARKETS_TO_DISPLAY = [
'SHIB-USD',
'COMP-USD',
'LDO-USD',
'NEAR-USD',
'APT-USD',
'SUI-USD',
'DOT-USD',
'ETC-USD',
'ARB-USD',
'CRV-USD',
'BLUR-USD',
'FIL-USD',
'XLM-USD',
'PEPE-USD',
'WLD-USD',
'SEI-USD',
'LTC-USD',
'BCH-USD',
];
+7
View File
@@ -122,13 +122,20 @@ export type NotificationDisplayData = {
toastDuration?: number;
};
export enum TransferNotificationTypes {
Withdrawal = 'withdrawal',
Deposit = 'deposit',
}
// Notification types
export type TransferNotifcation = {
txHash: string;
type?: TransferNotificationTypes;
toChainId?: string;
fromChainId?: string;
toAmount?: number;
triggeredAt?: number;
isCctp?: boolean;
errorCount?: number;
status?: StatusResponse;
};
+11 -1
View File
@@ -2,7 +2,7 @@ import { useCallback, useContext, createContext, useEffect, useState, useMemo }
import { useDispatch } from 'react-redux';
import { AES, enc } from 'crypto-js';
import { LocalWallet, type Subaccount } from '@dydxprotocol/v4-client-js';
import { NOBLE_BECH32_PREFIX, LocalWallet, type Subaccount } from '@dydxprotocol/v4-client-js';
import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account';
import { DialogTypes } from '@/constants/dialogs';
@@ -219,6 +219,16 @@ const useAccountsContext = () => {
else abacusStateManager.attemptDisconnectAccount();
}, [localDydxWallet]);
useEffect(() => {
const setNobleWallet = async () => {
if (hdKey?.mnemonic) {
const nobleWallet = await LocalWallet.fromMnemonic(hdKey.mnemonic, NOBLE_BECH32_PREFIX);
abacusStateManager.setNobleWallet(nobleWallet);
}
};
setNobleWallet();
}, [hdKey?.mnemonic]);
// clear subaccounts when no dydxAddress is set
useEffect(() => {
(async () => {
+11 -9
View File
@@ -1,11 +1,11 @@
import { createContext, useContext, useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import type { StatusResponse } from '@0xsquid/sdk';
import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage';
import { type TransferNotifcation } from '@/constants/notifications';
import { useAccounts } from '@/hooks/useAccounts';
import { STATUS_ERROR_GRACE_PERIOD, useSquid } from '@/hooks/useSquid';
import { fetchSquidStatus, STATUS_ERROR_GRACE_PERIOD } from '@/lib/squid';
import { useLocalStorage } from './useLocalStorage';
@@ -68,8 +68,6 @@ const useLocalNotificationsContext = () => {
[transferNotifications]
);
const squid = useSquid();
useQuery({
queryKey: 'getTransactionStatus',
queryFn: async () => {
@@ -81,23 +79,27 @@ const useLocalNotificationsContext = () => {
toChainId,
fromChainId,
triggeredAt,
isCctp,
errorCount,
status: currentStatus,
} = transferNotification;
// @ts-ignore status.errors is not in the type definition but can be returned
// also error can some time come back as an empty object so we need to ignore for that
const hasErrors = !!currentStatus?.errors ||
(currentStatus?.error && Object.keys(currentStatus.error).length !== 0);
if (
// @ts-ignore status.errors is not in the type definition but can be returned
!currentStatus?.errors &&
!currentStatus?.error &&
!hasErrors &&
(!currentStatus?.squidTransactionStatus ||
currentStatus?.squidTransactionStatus === 'ongoing')
) {
try {
const status = await squid?.getStatus({
const status = await fetchSquidStatus({
transactionId: txHash,
toChainId,
fromChainId,
});
}, isCctp);
if (status) {
transferNotification.status = status;
+1 -1
View File
@@ -8,7 +8,7 @@ import {
MARKETS_TO_DISPLAY,
} from '@/constants/markets';
import { testFlags } from '@/hooks/useTestFlags';
import { testFlags } from '@/lib/testFlags';
import { getAssets } from '@/state/assetsSelectors';
import { getPerpetualMarkets } from '@/state/perpetualsSelectors';
+7 -6
View File
@@ -20,6 +20,7 @@ import {
type NotificationTypeConfig,
NotificationType,
DEFAULT_TOAST_AUTO_CLOSE_MS,
TransferNotificationTypes,
} from '@/constants/notifications';
import { useSelectedNetwork, useStringGetter } from '@/hooks';
@@ -152,20 +153,20 @@ export const notificationTypes: NotificationTypeConfig[] = [
useEffect(() => {
for (const transfer of transferNotifications) {
const { fromChainId, status, txHash, toAmount } = transfer;
const { fromChainId, status, txHash, toAmount, type } = transfer;
const isFinished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
const icon = <Icon iconName={isFinished ? IconName.Transfer : IconName.Clock} />;
const type =
const transferType = type ??
fromChainId === ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId
? 'withdrawal'
: 'deposit';
? TransferNotificationTypes.Withdrawal
: TransferNotificationTypes.Deposit;
const title = stringGetter({
key: {
deposit: isFinished ? STRING_KEYS.DEPOSIT : STRING_KEYS.DEPOSIT_IN_PROGRESS,
withdrawal: isFinished ? STRING_KEYS.WITHDRAW : STRING_KEYS.WITHDRAW_IN_PROGRESS,
}[type],
}[transferType],
});
const toChainEta = status?.toChain?.chainData?.estimatedRouteDuration || 0;
@@ -190,7 +191,7 @@ export const notificationTypes: NotificationTypeConfig[] = [
slotIcon={icon}
slotTitle={title}
transfer={transfer}
type={type}
type={transferType}
triggeredAt={transfer.triggeredAt}
notification={notification}
/>
-50
View File
@@ -1,50 +0,0 @@
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { Squid } from '@0xsquid/sdk';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { getSelectedNetwork } from '@/state/appSelectors';
export const NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
export const STATUS_ERROR_GRACE_PERIOD = 300_000;
const useSquidContext = () => {
const selectedNetwork = useSelector(getSelectedNetwork);
const [_, setInitialized] = useState(false);
const initializeClient = async () => {
setInitialized(false);
if (!squid) return;
await squid.init();
setInitialized(true);
};
const squid = useMemo(
() =>
new Squid({
baseUrl: ENVIRONMENT_CONFIG_MAP[selectedNetwork]?.endpoints['0xsquid'],
integratorId: ENVIRONMENT_CONFIG_MAP[selectedNetwork]?.squidIntegratorId,
}),
[selectedNetwork]
);
useEffect(() => {
if (squid) {
initializeClient();
}
}, [squid]);
return squid;
};
type SquidContextType = ReturnType<typeof useSquidContext>;
const SquidContext = createContext<SquidContextType | undefined>(undefined);
SquidContext.displayName = '0xSquid';
export const SquidProvider = ({ ...props }) => (
<SquidContext.Provider value={useSquidContext()} {...props} />
);
export const useSquid = () => useContext(SquidContext);
+21 -3
View File
@@ -31,6 +31,7 @@ import { log } from '@/lib/telemetry';
import { useAccounts } from './useAccounts';
import { useTokenConfigs } from './useTokenConfigs';
import { useDydxClient } from './useDydxClient';
import { hashFromTx } from '@/lib/hashfromTx';
type SubaccountContextType = ReturnType<typeof useSubaccountContext>;
const SubaccountContext = createContext<SubaccountContextType>({} as SubaccountContextType);
@@ -293,12 +294,29 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
);
const sendSquidWithdraw = useCallback(
async (amount: number, payload: string) => {
async (amount: number, payload: string, isCctp?: boolean) => {
const cctpWithdraw = () => {
return new Promise<string>((resolve, reject) =>
abacusStateManager.cctpWithdraw((success, error, data) => {
const parsedData = JSON.parse(data);
if (success && parsedData?.code == 0) {
resolve(parsedData?.transactionHash);
} else {
reject(error);
}
})
)
}
if (isCctp) {
return await cctpWithdraw();
}
if (!subaccountClient) {
return;
}
return await sendSquidWithdrawFromSubaccount({ subaccountClient, amount, payload });
const tx = await sendSquidWithdrawFromSubaccount({ subaccountClient, amount, payload });
return hashFromTx(tx?.hash);
},
[subaccountClient, sendSquidWithdrawFromSubaccount]
);
-53
View File
@@ -1,53 +0,0 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import queryString from 'query-string';
class TestFlags {
public queryParams: queryString.ParsedQuery<string>;
constructor() {
this.queryParams = {};
}
setQueryParams(flags: queryString.ParsedQuery<string>) {
this.queryParams = flags;
}
get displayInitializingMarkets() {
return !!this.queryParams.displayInitializingMarkets;
}
/**
* @description We are temporarily displaying a subset of ACTIVE markets, this flag will allow you to see all ACTIVE markets
*/
get displayAllMarkets() {
return !!this.queryParams.displayAllMarkets;
}
}
export const testFlags = new TestFlags();
const useTestFlagsContext = () => {
const [queryParams, setQueryParams] = useState<queryString.ParsedQuery<string>>({});
const location = useLocation();
useEffect(() => {
const parsedQueryParams = queryString.parse(location.search.substring(-1));
testFlags.setQueryParams(parsedQueryParams);
setQueryParams(parsedQueryParams);
}, []);
return {
...queryParams,
};
};
type TestFlagsContextType = ReturnType<typeof useTestFlagsContext>;
const TestFlagsContext = createContext<TestFlagsContextType>({} as TestFlagsContextType);
TestFlagsContext.displayName = 'TestFlags';
export const TestFlagsProvider = ({ ...props }) => (
<TestFlagsContext.Provider value={useTestFlagsContext()} {...props} />
);
export const useTestFlags = () => useContext(TestFlagsContext);
+3 -6
View File
@@ -11,22 +11,19 @@ export const useTradeFormInputs = () => {
const { limitPriceInput, triggerPriceInput, trailingPercentInput } = tradeFormInputValues;
useEffect(() => {
const floatValue = parseFloat(triggerPriceInput);
abacusStateManager.setTradeValue({
value: floatValue,
value: triggerPriceInput,
field: TradeInputField.triggerPrice,
});
}, [triggerPriceInput]);
useEffect(() => {
const floatValue = parseFloat(limitPriceInput);
abacusStateManager.setTradeValue({ value: floatValue, field: TradeInputField.limitPrice });
abacusStateManager.setTradeValue({ value: limitPriceInput, field: TradeInputField.limitPrice });
}, [limitPriceInput]);
useEffect(() => {
const floatValue = parseFloat(trailingPercentInput);
abacusStateManager.setTradeValue({
value: floatValue,
value: trailingPercentInput,
field: TradeInputField.trailingPercent,
});
}, [trailingPercentInput]);
+165 -1
View File
@@ -1,7 +1,8 @@
import Abacus, { type Nullable } from '@dydxprotocol/v4-abacus';
import Long from 'long';
import type { IndexedTx } from '@cosmjs/stargate';
import { encodeJson } from '@dydxprotocol/v4-client-js';
import { GAS_MULTIPLIER, encodeJson } from '@dydxprotocol/v4-client-js';
import { EncodeObject } from '@cosmjs/proto-signing';
import {
CompositeClient,
@@ -9,6 +10,7 @@ import {
type LocalWallet,
Network,
NetworkOptimizer,
NobleClient,
SubaccountClient,
ValidatorConfig,
OrderType,
@@ -40,11 +42,14 @@ import { openDialog } from '@/state/dialogs';
import { StatefulOrderError } from '../errors';
import { bytesToBigInt } from '../numbers';
import { log } from '../telemetry';
import { hashFromTx } from '../hashfromTx';
class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
private compositeClient: CompositeClient | undefined;
private nobleClient: NobleClient | undefined;
private store: RootStore | undefined;
private localWallet: LocalWallet | undefined;
private nobleWallet: LocalWallet | undefined;
constructor() {
this.compositeClient = undefined;
@@ -59,6 +64,13 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
this.localWallet = localWallet;
}
setNobleWallet(nobleWallet: LocalWallet) {
this.nobleWallet = nobleWallet;
if (this.nobleClient) {
this.nobleClient.connect(nobleWallet);
}
}
async connectNetwork(
paramsInJson: Nullable<string>,
callback: (p0: Nullable<string>) => void
@@ -70,6 +82,7 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
websocketUrl,
validatorUrl,
chainId,
nobleValidatorUrl,
USDC_DENOM,
USDC_DECIMALS,
USDC_GAS_DENOM,
@@ -101,6 +114,10 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
this.compositeClient = compositeClient;
if (nobleValidatorUrl) {
this.nobleClient = new NobleClient(nobleValidatorUrl);
if (this.nobleWallet) await this.nobleClient.connect(this.nobleWallet);
}
// Dispatch custom event to notify other parts of the app that the network has been connected
const customEvent = new CustomEvent('abacus:connectNetwork', {
detail: parsedParams,
@@ -325,6 +342,130 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
}
}
async sendNobleIBC(
params: {
msgTypeUrl: string,
msg: any,
}
): Promise<string> {
if (!this.nobleClient?.isConnected) {
throw new Error('Missing nobleClient or localWallet');
}
try {
const ibcMsg = {
typeUrl: params.msgTypeUrl, // '/ibc.applications.transfer.v1.MsgTransfer',
value: params.msg,
};
const fee = await this.nobleClient.simulateTransaction([ibcMsg]);
// take out fee from amount before sweeping
const amount = parseInt(ibcMsg.value.token.amount, 10) -
Math.floor(parseInt(fee.amount[0].amount, 10) * GAS_MULTIPLIER);
if (amount <= 0) {
throw new Error('noble balance does not cover fees');
}
ibcMsg.value.token.amount = amount.toString();
const tx = await this.nobleClient.send([ibcMsg]);
const parsedTx = this.parseToPrimitives(tx);
return JSON.stringify(parsedTx);
} catch (error) {
log('DydxChainTransactions/sendNobleIBC', error);
return JSON.stringify({
error,
});
}
}
async withdrawToNobleIBC(
params: {
subaccountNumber: number,
amount: string,
ibcPayload: string,
}
): Promise<string> {
if (!this.compositeClient || !this.localWallet) {
throw new Error('Missing compositeClient or localWallet');
}
const { subaccountNumber, amount, ibcPayload } = params ?? {};
const parsedIbcPayload: {
msgTypeUrl: string,
msg: any,
} = ibcPayload ? JSON.parse(ibcPayload) : undefined;
try {
const msg = this.compositeClient.withdrawFromSubaccountMessage(
new SubaccountClient(this.localWallet, subaccountNumber),
parseFloat(amount).toFixed(this.compositeClient.validatorClient.config.denoms.USDC_DECIMALS)
);
const ibcMsg: EncodeObject = {
typeUrl: parsedIbcPayload.msgTypeUrl,
value: parsedIbcPayload.msg,
};
const tx = await this.compositeClient.send(
this.localWallet,
() => Promise.resolve([msg, ibcMsg]),
false
);
return JSON.stringify({
txHash: hashFromTx(tx?.hash)
});
} catch (error) {
log('DydxChainTransactions/withdrawToNobleIBC', error);
return JSON.stringify({
error,
});
}
}
async cctpWithdraw(params: {
typeUrl: string,
value: any,
}): Promise<string> {
if (!this.nobleClient?.isConnected) {
throw new Error('Missing nobleClient or localWallet');
}
try {
const ibcMsg = {
typeUrl: params.typeUrl, // '/circle.cctp.v1.MsgDepositForBurn',
value: params.value,
};
const fee = await this.nobleClient.simulateTransaction([ibcMsg]);
// take out fee from amount before sweeping
const amount = parseInt(ibcMsg.value.amount, 10) -
Math.floor(parseInt(fee.amount[0].amount, 10) * GAS_MULTIPLIER);
if (amount <= 0) {
throw new Error('noble balance does not cover fees');
}
ibcMsg.value.amount = amount.toString();
const tx = await this.nobleClient.send([ibcMsg]);
const parsedTx = this.parseToPrimitives(tx);
return JSON.stringify(parsedTx);
} catch (error) {
log('DydxChainTransactions/cctpWithdraw', error);
return JSON.stringify({
error,
});
}
}
async transaction(
type: TransactionTypes,
paramsInJson: Abacus.Nullable<string>,
@@ -354,6 +495,22 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
callback(result);
break;
}
case TransactionType.SendNobleIBC: {
const result = await this.sendNobleIBC(params);
callback(result);
break;
}
case TransactionType.WithdrawToNobleIBC: {
const result = await this.withdrawToNobleIBC(params);
callback(result);
break;
}
case TransactionType.CctpWithdraw: {
const result = await this.cctpWithdraw(params);
callback(result);
break;
break;
}
default: {
break;
}
@@ -441,6 +598,13 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
const parseDelegations = this.parseToPrimitives(delegations);
callback(JSON.stringify(parseDelegations));
break;
case QueryType.GetNobleBalance:
if (this.nobleClient?.isConnected) {
const nobleBalance = await this.nobleClient.getAccountBalance('uusdc');
const parsedNobleBalance = this.parseToPrimitives(nobleBalance);
callback(JSON.stringify(parsedNobleBalance));
}
break;
default:
break;
}
+22 -14
View File
@@ -26,13 +26,15 @@ import {
} from '@/constants/abacus';
import { DEFAULT_MARKETID } from '@/constants/markets';
import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork } from '@/constants/networks';
import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork, isMainnet } from '@/constants/networks';
import { CLEARED_SIZE_INPUTS, CLEARED_TRADE_INPUTS } from '@/constants/trade';
import type { RootStore } from '@/state/_store';
import { setTradeFormInputs } from '@/state/inputs';
import { getInputTradeOptions, getTransferInputs } from '@/state/inputsSelectors';
import { testFlags } from '@/lib/testFlags';
import AbacusRest from './rest';
import AbacusAnalytics from './analytics';
import AbacusWebsocket from './websocket';
@@ -81,10 +83,14 @@ class AbacusStateManager {
this.abacusFormatter
);
const appConfigs = AbacusAppConfig.Companion.forWeb;
if (!isMainnet || testFlags.withCCTP)
appConfigs.squidVersion = AbacusAppConfig.SquidVersion.V2;
this.stateManager = new AsyncAbacusStateManager(
'',
CURRENT_ABACUS_DEPLOYMENT,
AbacusAppConfig.Companion.forWeb,
appConfigs,
ioImplementations,
uiImplementations,
// @ts-ignore
@@ -180,6 +186,12 @@ class AbacusStateManager {
}
};
setNobleWallet = (nobleWallet?: LocalWallet) => {
if (nobleWallet) {
this.chainTransactions.setNobleWallet(nobleWallet);
}
};
setTransfersSourceAddress = (evmAddress: string) => {
this.stateManager.sourceAddress = evmAddress;
};
@@ -223,18 +235,6 @@ class AbacusStateManager {
this.abacusFormatter.setLocaleSeparators({ group, decimal });
};
setTransferStatus = ({
hash,
fromChainId,
toChainId,
}: {
hash: string;
fromChainId?: string;
toChainId?: string;
}) => {
this.stateManager.transferStatus(hash, fromChainId, toChainId);
};
// ------ Transactions ------ //
placeOrder = (
@@ -262,6 +262,14 @@ class AbacusStateManager {
) => void
) => this.stateManager.cancelOrder(orderId, callback);
cctpWithdraw = (
callback: (
success: boolean,
parsingError: Nullable<ParsingError>,
data: string,
) => void
): void => this.stateManager.commitCCTPWithdraw(callback);
// ------ Utils ------ //
getHistoricalPnlPeriod = (): Nullable<HistoricalPnlPeriods> =>
this.stateManager.historicalPnlPeriod;
+1 -1
View File
@@ -10,7 +10,7 @@ import {
} from '@/constants/websocket';
import { lastSuccessfulWebsocketRequestByOrigin } from '@/hooks/useAnalytics';
import { testFlags } from '@/hooks/useTestFlags';
import { testFlags } from '@/lib/testFlags';
import { subscriptionsByChannelId } from '@/lib/tradingView/dydxfeed/cache';
import { mapCandle } from '@/lib/tradingView/utils';
+3
View File
@@ -0,0 +1,3 @@
export const hashFromTx = (
txHash: string | Uint8Array
): string => `0x${Buffer.from(txHash).toString('hex')}`;
+48
View File
@@ -0,0 +1,48 @@
import { isMainnet } from '@/constants/networks';
import { GetStatus, StatusResponse } from '@0xsquid/sdk';
export const NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
export const STATUS_ERROR_GRACE_PERIOD = 300_000;
const getSquidStatusUrl = (isV2: boolean) => {
if (isV2) {
return isMainnet
? 'https://v2.api.squidrouter.com/v2/status'
: 'https://testnet.v2.api.squidrouter.com/v2/status';
}
return isMainnet
? 'https://api.squidrouter.com/v1/status'
: 'https://testnet.api.squidrouter.com/v1/status';
};
export const fetchSquidStatus = async (
params: GetStatus,
isV2?: boolean,
integratorId?: string
): Promise<StatusResponse> => {
const parsedParams: { [key: string]: string } = {
transactionId: params.transactionId,
fromChainId: String(params.fromChainId),
toChainId: String(params.toChainId),
};
if (isV2) parsedParams.bridgeType = 'cctp';
const url = `${getSquidStatusUrl(!!isV2)}?${new URLSearchParams(parsedParams).toString()}`;
const response = await fetch(url, {
headers: {
"x-integrator-id": integratorId || 'dYdX-api'
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error);
}
return response.json();
};
export const getNobleChainId = () => {
return isMainnet ? 'noble-1' : 'grand-1';
}
+31
View File
@@ -0,0 +1,31 @@
class TestFlags {
public queryParams: { [key: string]: string };
constructor() {
this.queryParams = {};
const hash = window.location.hash;
const queryIndex = hash.indexOf('?');
if (queryIndex === -1) return
const queryParamsString = hash.substring(queryIndex + 1);
const params = new URLSearchParams(queryParamsString);
for (const [key, value] of params) {
this.queryParams[key] = value;
}
}
get displayInitializingMarkets() {
return !!this.queryParams.displayInitializingMarkets;
}
get displayAllMarkets() {
return !!this.queryParams.displayAllMarkets;
}
get withCCTP() {
return !!this.queryParams.withCCTP;
}
}
export const testFlags = new TestFlags();
+4 -1
View File
@@ -47,7 +47,10 @@ export const inputsSlice = createSlice({
inputErrors: errors?.toArray(),
tradeInputs: trade,
closePositionInputs: closePosition,
transferInputs: transfer,
transferInputs: {
...transfer,
isCctp: !!transfer?.isCctp,
} as Nullable<TransferInputs>,
};
},
+1 -1
View File
@@ -4,7 +4,7 @@ import { shallowEqual, useSelector } from 'react-redux';
import { STRING_KEYS } from '@/constants/localization';
import { MARKETS_TO_DISPLAY } from '@/constants/markets';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { testFlags } from '@/hooks/useTestFlags';
import { testFlags } from '@/lib/testFlags';
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -18,7 +18,6 @@ import type { EvmAddress } from '@/constants/wallets';
import { useAccounts, useDebounce, useStringGetter, useSelectedNetwork } from '@/hooks';
import { useAccountBalance, CHAIN_DEFAULT_TOKEN_ADDRESS } from '@/hooks/useAccountBalance';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { NATIVE_TOKEN_ADDRESS, useSquid } from '@/hooks/useSquid';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
@@ -38,6 +37,7 @@ import { getTransferInputs } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers';
import { getNobleChainId, NATIVE_TOKEN_ADDRESS } from '@/lib/squid';
import { log } from '@/lib/telemetry';
import { parseWalletError } from '@/lib/wallet';
@@ -69,6 +69,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
summary,
errors: routeErrors,
errorMessage: routeErrorMessage,
isCctp,
} = useSelector(getTransferInputs, shallowEqual) || {};
const chainId = chainIdStr ? parseInt(chainIdStr) : undefined;
@@ -84,7 +85,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
);
const [fromAmount, setFromAmount] = useState('');
const [slippage, setSlippage] = useState(0.01); // 1% slippage
const [slippage, setSlippage] = useState(isCctp ? 0 : 0.01); // 1% slippage
const debouncedAmount = useDebounce<string>(fromAmount, 500);
// Async Data
@@ -99,6 +100,8 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const debouncedAmountBN = MustBigNumber(debouncedAmount);
const balanceBN = MustBigNumber(balance);
useEffect(() => setSlippage(isCctp ? 0 : 0.01), [isCctp]);
useEffect(() => {
const hasInvalidInput =
debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0) || debouncedAmountBN.gt(balanceBN);
@@ -250,11 +253,11 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
if (txHash) {
addTransferNotification({
txHash: txHash,
toChainId: ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId,
toChainId: !isCctp ? ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId : getNobleChainId(),
fromChainId: chainIdStr || undefined,
toAmount: summary?.usdcSize || undefined,
triggeredAt: Date.now(),
notificationStatus: NotificationStatus.Triggered,
isCctp,
});
abacusStateManager.clearTransferInputValues();
setFromAmount('');
@@ -109,6 +109,8 @@ export const DepositButtonAndReceipt = ({
? stringGetter({ key: STRING_KEYS.HIDE_ALL_DETAILS })
: stringGetter({ key: STRING_KEYS.SHOW_ALL_DETAILS });
const totalFees = (summary?.bridgeFee || 0) + (summary?.gasFee || 0);
const submitButtonReceipt = [
{
key: 'equity',
@@ -158,9 +160,7 @@ export const DepositButtonAndReceipt = ({
{
key: 'total-fees',
label: <span>{stringGetter({ key: STRING_KEYS.TOTAL_FEES })}</span>,
value: typeof summary?.bridgeFee === 'number' && typeof summary?.gasFee === 'number' && (
<Output type={OutputType.Fiat} value={summary?.bridgeFee + summary?.gasFee} />
),
value: <Output type={OutputType.Fiat} value={totalFees} />,
subitems: feeSubitems,
},
{
@@ -183,7 +183,12 @@ export const DepositButtonAndReceipt = ({
type={OutputType.Text}
value={stringGetter({
key: STRING_KEYS.X_MINUTES_LOWERCASED,
params: { X: Math.round(summary?.estimatedRouteDuration / 60) },
params: {
X:
summary?.estimatedRouteDuration < 60
? '< 1'
: Math.round(summary?.estimatedRouteDuration / 60),
},
})}
/>
),
@@ -10,7 +10,7 @@ import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { NotificationStatus } from '@/constants/notifications';
import { NotificationStatus, TransferNotificationTypes } from '@/constants/notifications';
import { NumberSign } from '@/constants/numbers';
import {
@@ -45,6 +45,7 @@ import { getTransferInputs } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers';
import { getNobleChainId } from '@/lib/squid';
import { TokenSelectMenu } from './TokenSelectMenu';
import { WithdrawButtonAndReceipt } from './WithdrawForm/WithdrawButtonAndReceipt';
@@ -58,11 +59,6 @@ export const WithdrawForm = () => {
const { sendSquidWithdraw } = useSubaccount();
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
// User input
const [withdrawAmount, setWithdrawAmount] = useState('');
const [slippage, setSlippage] = useState(0.01); // 0.1% slippage
const debouncedAmount = useDebounce<string>(withdrawAmount, 500);
const {
requestPayload,
token,
@@ -71,8 +67,15 @@ export const WithdrawForm = () => {
resources,
errors: routeErrors,
errorMessage: routeErrorMessage,
isCctp
} = useSelector(getTransferInputs, shallowEqual) || {};
// User input
const [withdrawAmount, setWithdrawAmount] = useState('');
const [slippage, setSlippage] = useState(isCctp ? 0 : 0.01); // 0.1% slippage
const debouncedAmount = useDebounce<string>(withdrawAmount, 500);
const isValidAddress = toAddress && isAddress(toAddress);
const toToken = useMemo(
@@ -89,6 +92,8 @@ export const WithdrawForm = () => {
[freeCollateral?.current]
);
useEffect(() => setSlippage(isCctp ? 0 : 0.01), [isCctp]);
useEffect(() => {
abacusStateManager.setTransferValue({
field: TransferInputField.type,
@@ -163,16 +168,16 @@ export const WithdrawForm = () => {
})
);
} else {
const txHash = await sendSquidWithdraw(debouncedAmountBN.toNumber(), requestPayload.data);
if (txHash?.hash) {
const hash = `0x${Buffer.from(txHash.hash).toString('hex')}`;
const txHash = await sendSquidWithdraw(debouncedAmountBN.toNumber(), requestPayload.data, isCctp);
if (txHash) {
addTransferNotification({
txHash: hash,
fromChainId: ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId,
txHash: txHash,
type: TransferNotificationTypes.Withdrawal,
fromChainId: !isCctp ? ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId : getNobleChainId(),
toChainId: chainIdStr || undefined,
toAmount: debouncedAmountBN.toNumber(),
triggeredAt: Date.now(),
notificationStatus: NotificationStatus.Triggered,
isCctp,
});
abacusStateManager.clearTransferInputValues();
setWithdrawAmount('');
@@ -89,14 +89,14 @@ export const WithdrawButtonAndReceipt = ({
const showSubitemsToggle = showFeeBreakdown
? stringGetter({ key: STRING_KEYS.HIDE_ALL_DETAILS })
: stringGetter({ key: STRING_KEYS.SHOW_ALL_DETAILS });
const totalFees = (summary?.bridgeFee || 0) + (summary?.gasFee || 0);
const submitButtonReceipt = [
{
key: 'total-fees',
label: <span>{stringGetter({ key: STRING_KEYS.TOTAL_FEES })}</span>,
value: typeof summary?.bridgeFee === 'number' && typeof summary?.gasFee === 'number' && (
<Output type={OutputType.Fiat} value={summary?.bridgeFee + summary?.gasFee} />
),
value: <Output type={OutputType.Fiat} value={totalFees} />,
subitems: feeSubitems,
},
{
@@ -165,7 +165,12 @@ export const WithdrawButtonAndReceipt = ({
type={OutputType.Text}
value={stringGetter({
key: STRING_KEYS.X_MINUTES_LOWERCASED,
params: { X: Math.round(summary?.estimatedRouteDuration / 60) },
params: {
X:
summary?.estimatedRouteDuration < 60
? '< 1'
: Math.round(summary?.estimatedRouteDuration / 60),
},
})}
/>
),
+2 -2
View File
@@ -206,7 +206,7 @@ export const TradeForm = ({
dispatch(setTradeFormInputs({ triggerPriceInput: value }));
},
value: triggerPriceInput ?? '',
decimals: tickSizeDecimals || USD_DECIMALS,
decimals: tickSizeDecimals ?? USD_DECIMALS,
});
}
@@ -226,7 +226,7 @@ export const TradeForm = ({
dispatch(setTradeFormInputs({ limitPriceInput: value }));
},
value: limitPriceInput,
decimals: tickSizeDecimals || USD_DECIMALS,
decimals: tickSizeDecimals ?? USD_DECIMALS,
});
}
+8 -7
View File
@@ -1,10 +1,10 @@
import { type Dispatch, type SetStateAction, useCallback, useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import styled, { AnyStyledComponent, css } from 'styled-components';
import { Root, Thumb, Track } from '@radix-ui/react-slider';
import _ from 'lodash';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { type Nullable, TradeInputField } from '@/constants/abacus';
import { TradeInputField } from '@/constants/abacus';
import { PositionSide } from '@/constants/trade';
import { MustBigNumber, type BigNumberish } from '@/lib/numbers';
@@ -12,11 +12,11 @@ import abacusStateManager from '@/lib/abacus';
type ElementProps = {
leverage?: BigNumberish | null;
leverageInputValue: Nullable<number>;
leverageInputValue: string;
maxLeverage: BigNumberish | null;
orderSide: OrderSide;
positionSide: PositionSide;
setLeverageInputValue: Dispatch<SetStateAction<Nullable<number>>>;
setLeverageInputValue: (value: string) => void;
};
type StyleProps = { className?: string };
@@ -33,6 +33,7 @@ export const LeverageSlider = ({
const leverageBN = MustBigNumber(leverage);
const maxLeverageBN = MustBigNumber(maxLeverage);
const leverageInputBN = MustBigNumber(leverageInputValue || leverage);
const leverageInputNumber = isNaN(leverageInputBN.toNumber()) ? 0 : leverageInputBN.toNumber();
const sliderConfig = useMemo(
() => ({
@@ -79,12 +80,12 @@ export const LeverageSlider = ({
);
const onSliderDrag = ([newLeverage]: number[]) => {
setLeverageInputValue(newLeverage);
setLeverageInputValue(`${newLeverage}`);
debouncedSetAbacusLeverage(newLeverage);
};
const onValueCommit = ([newLeverage]: number[]) => {
setLeverageInputValue(newLeverage);
setLeverageInputValue(`${newLeverage}`);
// Ensure Abacus is updated with the latest, committed value
debouncedSetAbacusLeverage.cancel();
@@ -102,7 +103,7 @@ export const LeverageSlider = ({
min={min}
max={max}
step={0.1}
value={[Math.min(Math.max(leverageInputBN.toNumber(), min), max)]}
value={[Math.min(Math.max(leverageInputNumber, min), max)]}
onValueChange={onSliderDrag}
onValueCommit={onValueCommit}
>
@@ -1,9 +1,8 @@
import type { Dispatch, SetStateAction } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import styled, { AnyStyledComponent } from 'styled-components';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { TradeInputField, Nullable } from '@/constants/abacus';
import { TradeInputField } from '@/constants/abacus';
import { ButtonShape } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { LEVERAGE_DECIMALS } from '@/constants/numbers';
@@ -62,21 +61,7 @@ export const MarketLeverageInput = ({
const leveragePosition = postOrderLeverage ? newPositionSide : currentPositionSide;
const onLeverageInput = ({ value, floatValue }: { value: string; floatValue?: number }) => {
const newLeverage = MustBigNumber(floatValue).toFixed();
if (value === '' || newLeverage === 'NaN' || !floatValue) {
setLeverageInputValue('');
abacusStateManager.setTradeValue({
value: null,
field: TradeInputField.leverage,
});
} else {
updateLeverage(floatValue);
}
};
const updateLeverage = (newLeverage: string | number) => {
const getSignedLeverage = (newLeverage: string | number) => {
const newLeverageBN = MustBigNumber(newLeverage);
const newLeverageSignedBN =
leveragePosition === PositionSide.Short ||
@@ -84,10 +69,33 @@ export const MarketLeverageInput = ({
? newLeverageBN.abs().negated()
: newLeverageBN.abs();
setLeverageInputValue(newLeverageSignedBN.toString());
return newLeverageSignedBN.toFixed(LEVERAGE_DECIMALS);
};
const onLeverageInput = ({
floatValue,
formattedValue,
}: {
floatValue?: number;
formattedValue: string;
}) => {
setLeverageInputValue(formattedValue);
const newLeverage = MustBigNumber(floatValue).toFixed();
abacusStateManager.setTradeValue({
value: newLeverageSignedBN.toFixed(LEVERAGE_DECIMALS),
value:
formattedValue === '' || newLeverage === 'NaN' ? null : getSignedLeverage(formattedValue),
field: TradeInputField.leverage,
});
};
const updateLeverage = (newLeverage: string | number) => {
const newLeverageSigned = getSignedLeverage(newLeverage);
setLeverageInputValue(newLeverageSigned);
abacusStateManager.setTradeValue({
value: newLeverageSigned,
field: TradeInputField.leverage,
});
};
@@ -130,7 +138,7 @@ export const MarketLeverageInput = ({
>
<Styled.LeverageSlider
leverage={currentLeverage}
leverageInputValue={leverageInputValue}
leverageInputValue={getSignedLeverage(leverageInputValue)}
maxLeverage={maxLeverage}
orderSide={orderSide}
positionSide={currentPositionSide}
+20 -8
View File
@@ -49,7 +49,7 @@ export const TradeSizeInputs = () => {
useSelector(getCurrentMarketConfig, shallowEqual) || {};
const { size, usdcSize, leverage, input: lastEditedInput } = inputTradeSizeData || {};
const { needsLeverage } = currentTradeInputOptions || {};
const decimals = stepSizeDecimals || TOKEN_DECIMALS;
const decimals = stepSizeDecimals ?? TOKEN_DECIMALS;
const { amountInput, usdAmountInput, leverageInput } = useSelector(
getTradeFormInputs,
@@ -70,22 +70,34 @@ export const TradeSizeInputs = () => {
}
}, [size, usdcSize, leverage, lastEditedInput]);
const onSizeInput = ({ value, floatValue }: { value: string; floatValue?: number }) => {
dispatch(setTradeFormInputs({ amountInput: value }));
const onSizeInput = ({
floatValue,
formattedValue,
}: {
floatValue?: number;
formattedValue: string;
}) => {
dispatch(setTradeFormInputs({ amountInput: formattedValue }));
const newAmount = MustBigNumber(floatValue).toFixed(decimals);
abacusStateManager.setTradeValue({
value: value === '' || newAmount === 'NaN' ? null : newAmount,
value: formattedValue === '' || newAmount === 'NaN' ? null : newAmount,
field: TradeInputField.size,
});
};
const onUSDCInput = ({ value, floatValue }: { value: string; floatValue?: number }) => {
dispatch(setTradeFormInputs({ usdAmountInput: value }));
const newUsdcAmount = MustBigNumber(floatValue).toFixed();
const onUSDCInput = ({
floatValue,
formattedValue,
}: {
floatValue?: number;
formattedValue: string;
}) => {
dispatch(setTradeFormInputs({ usdAmountInput: formattedValue }));
const newUsdcAmount = MustBigNumber(floatValue).toFixed(tickSizeDecimals || USD_DECIMALS);
abacusStateManager.setTradeValue({
value: value === '' || newUsdcAmount === 'NaN' ? null : newUsdcAmount,
value: formattedValue === '' || newUsdcAmount === 'NaN' ? null : newUsdcAmount,
field: TradeInputField.usdcSize,
});
};
@@ -12,10 +12,11 @@ import { LoadingSpinner } from '@/components/Loading/LoadingSpinner';
import { layoutMixins } from '@/styles/layoutMixins';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { TransferNotificationTypes } from '@/constants/notifications';
type ElementProps = {
status?: StatusResponse;
type: 'withdrawal' | 'deposit';
type: TransferNotificationTypes;
};
type StyleProps = {
@@ -46,11 +47,13 @@ export const TransferStatusSteps = ({ className, status, type }: ElementProps &
{
label: stringGetter({
key:
type === 'deposit' ? STRING_KEYS.INITIATED_DEPOSIT : STRING_KEYS.INITIATED_WITHDRAWAL,
type === TransferNotificationTypes.Deposit
? STRING_KEYS.INITIATED_DEPOSIT
: STRING_KEYS.INITIATED_WITHDRAWAL,
}),
step: TransferStatusStep.FromChain,
link:
type === 'deposit'
link:
type === TransferNotificationTypes.Deposit
? status?.fromChain?.transactionUrl
: routeStatus?.[0]?.chainId === dydxChainId && routeStatus[0].txHash
? `${mintscanTxUrl?.replace('{tx_hash}', routeStatus[0].txHash.replace('0x', ''))}`
@@ -63,14 +66,20 @@ export const TransferStatusSteps = ({ className, status, type }: ElementProps &
},
{
label: stringGetter({
key: type === 'deposit' ? STRING_KEYS.DEPOSIT_TO_CHAIN : STRING_KEYS.WITHDRAW_TO_CHAIN,
key:
type === TransferNotificationTypes.Deposit
? STRING_KEYS.DEPOSIT_TO_CHAIN
: STRING_KEYS.WITHDRAW_TO_CHAIN,
params: {
CHAIN: type === 'deposit' ? 'dYdX' : status?.toChain?.chainData?.chainName,
CHAIN:
type === TransferNotificationTypes.Deposit
? 'dYdX'
: status?.toChain?.chainData?.chainName,
},
}),
step: TransferStatusStep.ToChain,
link:
type === 'withdrawal'
type === TransferNotificationTypes.Withdrawal
? status?.toChain?.transactionUrl
: currentStatus?.chainId === dydxChainId && currentStatus?.txHash
? `${mintscanTxUrl?.replace('{tx_hash}', currentStatus.txHash.replace('0x', ''))}`
@@ -5,7 +5,7 @@ import { useInterval, useStringGetter } from '@/hooks';
import { AlertType } from '@/constants/alerts';
import { STRING_KEYS } from '@/constants/localization';
import { TransferNotifcation } from '@/constants/notifications';
import { TransferNotifcation, TransferNotificationTypes } from '@/constants/notifications';
import { formatSeconds } from '@/lib/timeUtils';
@@ -22,7 +22,7 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { TransferStatusSteps } from './TransferStatusSteps';
type ElementProps = {
type: 'withdrawal' | 'deposit';
type: TransferNotificationTypes;
transfer: TransferNotifcation;
triggeredAt?: number;
};
@@ -44,6 +44,7 @@ export const TransferStatusNotification = ({
// @ts-ignore status.errors is not in the type definition but can be returned
const error = status?.errors?.length ? status?.errors[0] : status?.error;
const hasError = error && Object.keys(error).length !== 0;
const updateSecondsLeft = useCallback(() => {
const fromChainEta = (status?.fromChain?.chainData?.estimatedRouteDuration || 0) * 1000;
@@ -54,7 +55,7 @@ export const TransferStatusNotification = ({
useInterval({ callback: updateSecondsLeft });
const inProgressStatusString =
type === 'deposit'
type === TransferNotificationTypes.Deposit
? secondsLeft > 0
? STRING_KEYS.DEPOSIT_STATUS
: STRING_KEYS.DEPOSIT_STATUS_SHORTLY
@@ -63,7 +64,7 @@ export const TransferStatusNotification = ({
: STRING_KEYS.WITHDRAW_STATUS_SHORTLY;
const statusString =
type === 'deposit'
type === TransferNotificationTypes.Deposit
? status?.squidTransactionStatus === 'success'
? STRING_KEYS.DEPOSIT_COMPLETE
: inProgressStatusString
@@ -87,7 +88,7 @@ export const TransferStatusNotification = ({
},
})}
</Styled.Status>
{error && (
{hasError && (
<AlertMessage type={AlertType.Error}>
{stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
@@ -112,7 +113,7 @@ export const TransferStatusNotification = ({
) : (
<Styled.BridgingStatus>
{content}
{!isToast && status?.squidTransactionStatus !== 'success' && (
{!isToast && status?.squidTransactionStatus !== 'success' && !hasError && (
<Styled.TransferStatusSteps status={status} type={type} />
)}
</Styled.BridgingStatus>
@@ -120,8 +121,7 @@ export const TransferStatusNotification = ({
}
slotAction={
isToast &&
status &&
!error && (
status && (
<Styled.Trigger
isOpen={open}
onClick={(e: MouseEvent) => {
+69 -39
View File
@@ -42,7 +42,11 @@ type StyleProps = {
className?: string;
};
type RowData = OrderbookLine & { side: 'bid' | 'ask'; mine?: number };
type RowData = Pick<OrderbookLine, 'depth' | 'offset' | 'price' | 'size'> & {
side: 'bid' | 'ask';
mine?: number;
key: string;
};
const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number }) => {
const orderbook = useSelector(getCurrentMarketOrderbook, shallowEqual);
@@ -53,8 +57,9 @@ const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number
return useMemo(() => {
const asks = (orderbook?.asks?.toArray() ?? [])
.map(
(row: OrderbookLine) =>
(row: OrderbookLine, idx: number) =>
({
key: `ask-${idx}`,
side: 'ask',
mine: openOrdersBySideAndPrice[OrderSide.SELL]?.[row.price]?.size,
...row,
@@ -64,8 +69,9 @@ const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number
const bids = (orderbook?.bids?.toArray() ?? [])
.map(
(row: OrderbookLine) =>
(row: OrderbookLine, idx: number) =>
({
key: `bid-${idx}`,
side: 'bid',
mine: openOrdersBySideAndPrice[OrderSide.BUY]?.[row.price]?.size,
...row,
@@ -97,15 +103,44 @@ const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number
}
const spread =
asks[0] && bids[0] ? MustBigNumber(asks[0]?.price ?? 0).minus(bids[0]?.price ?? 0) : null;
asks[0]?.price && bids[0]?.price ? MustBigNumber(asks[0].price).minus(bids[0].price) : null;
const spreadPercent = orderbook?.spreadPercent;
const histogramRange = Math.max(
Number(bids[bids.length - 1]?.depth),
Number(asks[asks.length - 1]?.depth)
isNaN(Number(bids[bids.length - 1]?.depth)) ? 0 : Number(bids[bids.length - 1]?.depth),
isNaN(Number(asks[asks.length - 1]?.depth)) ? 0 : Number(asks[asks.length - 1]?.depth)
);
// Ensure asks and bids are of length maxRowsPerSide by adding empty rows.
let idx = asks.length - 1;
while (asks.length < maxRowsPerSide) {
idx += 1;
asks.push({
key: `ask-${idx}`,
side: 'ask',
size: 0,
price: 0,
offset: 0,
depth: 0,
});
}
idx = bids.length - 1;
while (bids.length < maxRowsPerSide) {
idx += 1;
bids.push({
key: `bid-${idx}`,
side: 'bid',
size: 0,
price: 0,
offset: 0,
depth: 0,
});
}
return { asks, bids, spread, spreadPercent, histogramRange, hasOrderbook: !!orderbook };
}, [orderbook, openOrdersBySideAndPrice]);
};
@@ -142,31 +177,33 @@ const OrderbookTable = ({
getCellValue: (row: RowData) => row.size,
label: stringGetter({ key: STRING_KEYS.ORDERBOOK_ORDER_SIZE }),
tag: symbol,
renderCell: (row: RowData) => (
<Styled.HistogramOutput
highlightText
type={OutputType.Asset}
value={row.size}
fractionDigits={stepSizeDecimals}
histogramSide={histogramSide === 'left' && 'left'}
useGrouping={false}
/>
),
renderCell: (row: RowData) =>
row.size > 0 && (
<Styled.HistogramOutput
highlightText
type={OutputType.Asset}
value={row.size}
fractionDigits={stepSizeDecimals}
histogramSide={histogramSide === 'left' && 'left'}
useGrouping={false}
/>
),
},
{
columnKey: 'price',
getCellValue: (row: RowData) => row.price,
label: stringGetter({ key: STRING_KEYS.PRICE }),
tag: 'USD',
renderCell: (row: RowData) => (
<OrderbookTradesOutput
highlightText
type={OutputType.Number}
value={row.price}
fractionDigits={tickSizeDecimals}
useGrouping={false}
/>
),
renderCell: (row: RowData) =>
row.price > 0 && (
<OrderbookTradesOutput
highlightText
type={OutputType.Number}
value={row.price}
fractionDigits={tickSizeDecimals}
useGrouping={false}
/>
),
},
{
columnKey: 'subaccount-orders',
@@ -194,12 +231,13 @@ const OrderbookTable = ({
label="Orderbook"
data={data}
columns={columns}
getRowKey={(row: RowData) => `${row.side}-${row.price}`}
getRowKey={(row: RowData) => row.key}
getRowAttributes={(row: RowData) => ({
'data-side': row.side,
style: {
'--histogram-bucket-size': row.size,
'--histogram-bucket-depth': row.depth,
'--tr-pointerEvents': row.price ? 'auto' : 'none',
},
})}
onRowAction={onRowAction}
@@ -238,19 +276,10 @@ export const Orderbook = ({
maxRowsPerSide,
});
const [showRowsPerSide, setShowRowsPerSide] = useState(0);
// Make rows visible one by one so avoid jumps in initial scroll position
useEffect(() => {
if (showRowsPerSide < maxRowsPerSide) {
setShowRowsPerSide((showRows) => showRows + 1);
}
}, [showRowsPerSide]);
const data = useMemo(
() =>
[
...bids.slice(0, showRowsPerSide).reverse(),
...bids.reverse(),
{
key: 'spread',
slotCustomRow: (props) => (
@@ -267,9 +296,9 @@ export const Orderbook = ({
</Styled.SpreadTableRow>
),
} as CustomRowConfig,
...asks.slice(0, showRowsPerSide),
...asks,
].reverse(),
[asks, bids, spread, spreadPercent, showRowsPerSide, isTablet]
[asks, bids, spread, spreadPercent, isTablet]
);
const onRowAction = useCallback(
@@ -325,7 +354,7 @@ export const Orderbook = ({
</Styled.Header>
<Styled.SplitOrderbook>
<OrderbookTable data={asks} histogramSide="right" {...orderbookTableProps} />
<OrderbookTable data={bids} histogramSide="left" {...orderbookTableProps} />
<OrderbookTable data={bids.reverse()} histogramSide="left" {...orderbookTableProps} />
</Styled.SplitOrderbook>
</Styled.HorizontalLayout>
);
@@ -495,6 +524,7 @@ Styled.OrderbookTable = styled(OrderbookTradesTable)<StyleProps>`
tr {
--histogram-bucket-depth: 0;
pointer-events: var(--tr-pointerEvents);
&[data-side='bid'] {
--accent-color: var(--color-positive);