Compare commits

...
Author SHA1 Message Date
Rui db3e7331b8 Fix path to chainLogo 2023-12-06 13:35:08 -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
30 changed files with 401 additions and 265 deletions
+2 -2
View File
@@ -39,8 +39,8 @@
"@cosmjs/proto-signing": "^0.31.0", "@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0", "@cosmjs/stargate": "^0.31.0",
"@cosmjs/tendermint-rpc": "^0.31.0", "@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/v4-abacus": "^1.0.30", "@dydxprotocol/v4-abacus": "^1.1.4",
"@dydxprotocol/v4-client-js": "^1.0.0", "@dydxprotocol/v4-client-js": "^1.0.6",
"@dydxprotocol/v4-localization": "^1.0.17", "@dydxprotocol/v4-localization": "^1.0.17",
"@ethersproject/providers": "^5.7.2", "@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3", "@js-joda/core": "^5.5.3",
+12 -12
View File
@@ -1,5 +1,9 @@
lockfileVersion: '6.0' lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies: dependencies:
'@0xsquid/sdk': '@0xsquid/sdk':
specifier: ^1.10.0 specifier: ^1.10.0
@@ -23,11 +27,11 @@ dependencies:
specifier: ^0.31.0 specifier: ^0.31.0
version: 0.31.0 version: 0.31.0
'@dydxprotocol/v4-abacus': '@dydxprotocol/v4-abacus':
specifier: ^1.0.30 specifier: ^1.1.4
version: 1.0.30 version: 1.1.4
'@dydxprotocol/v4-client-js': '@dydxprotocol/v4-client-js':
specifier: ^1.0.0 specifier: ^1.0.6
version: 1.0.0 version: 1.0.6
'@dydxprotocol/v4-localization': '@dydxprotocol/v4-localization':
specifier: ^1.0.17 specifier: ^1.0.17
version: 1.0.17 version: 1.0.17
@@ -984,12 +988,12 @@ packages:
resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==} resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==}
dev: true dev: true
/@dydxprotocol/v4-abacus@1.0.30: /@dydxprotocol/v4-abacus@1.1.4:
resolution: {integrity: sha512-zzJzNzMR3Stwv1Mu0XGUNlyo1V/ywo3yi3QSzE4mzUtXB+p5Shj5hRQCcwhHcNjLgp6a+T0BcRjBPa4QMETmwg==} resolution: {integrity: sha512-gml6qheFsPShE9p3FmzFeStYM9ZVhgMq0K0+y12V7pObnMKbumkOIISgCzc47idkah0GMNhiqwi9faD5SjOy/A==}
dev: false dev: false
/@dydxprotocol/v4-client-js@1.0.0: /@dydxprotocol/v4-client-js@1.0.6:
resolution: {integrity: sha512-ehfHO+zQy795TcJRwtiawadFHZyh4HnpJNP26hCGsIjLE5q6LLHweQHpK/1N/sXU1PBOuQwc7iaJnFop6gYauQ==} resolution: {integrity: sha512-xiWH+kbix+zhI6EsAnd+NDvkjBgxWtGwQmvpd0PjljWNYSFgUtNe5M+piDdRbl2nhy6YWbxAGTwwS3K/ih5qSw==}
dependencies: dependencies:
'@cosmjs/amino': 0.30.1 '@cosmjs/amino': 0.30.1
'@cosmjs/encoding': 0.31.1 '@cosmjs/encoding': 0.31.1
@@ -14136,7 +14140,3 @@ packages:
/zwitch@2.0.4: /zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: true dev: true
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+3 -3
View File
@@ -1,7 +1,7 @@
[ [
{ {
"chainId": "42161", "chainId": "42161",
"tokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "tokenAddress": "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8",
"name": "Arbitrum" "name": "Arbitrum"
}, },
{ {
@@ -11,7 +11,7 @@
}, },
{ {
"chainId": "8453", "chainId": "8453",
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "tokenAddress": "0x66627F389ae46D881773B7131139b2411980E09E",
"name": "Base" "name": "Base"
}, },
{ {
@@ -21,7 +21,7 @@
}, },
{ {
"chainId": "10", "chainId": "10",
"tokenAddress": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", "tokenAddress": "0x7F5c764cBc14f9669B88837ca1490cCa17c31607",
"name": "OP Mainnet" "name": "OP Mainnet"
}, },
{ {
+15 -15
View File
@@ -42,7 +42,7 @@
"ethereumChainId": "5", "ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet", "dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png", "chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api", "squidIntegratorId": "dYdX-api",
"isMainNet": false, "isMainNet": false,
"tokens": { "tokens": {
@@ -116,7 +116,7 @@
"ethereumChainId": "5", "ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet", "dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png", "chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api", "squidIntegratorId": "dYdX-api",
"isMainNet": false, "isMainNet": false,
"tokens": { "tokens": {
@@ -189,7 +189,7 @@
"ethereumChainId": "5", "ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet", "dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png", "chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api", "squidIntegratorId": "dYdX-api",
"isMainNet": false, "isMainNet": false,
"tokens": { "tokens": {
@@ -262,7 +262,7 @@
"ethereumChainId": "5", "ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet", "dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png", "chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api", "squidIntegratorId": "dYdX-api",
"isMainNet": false, "isMainNet": false,
"tokens": { "tokens": {
@@ -335,7 +335,7 @@
"ethereumChainId": "5", "ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet", "dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png", "chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api", "squidIntegratorId": "dYdX-api",
"isMainNet": false, "isMainNet": false,
"tokens": { "tokens": {
@@ -410,7 +410,7 @@
"ethereumChainId": "5", "ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet", "dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png", "chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api", "squidIntegratorId": "dYdX-api",
"isMainNet": false, "isMainNet": false,
"tokens": { "tokens": {
@@ -483,7 +483,7 @@
"ethereumChainId": "5", "ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet", "dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png", "chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api", "squidIntegratorId": "dYdX-api",
"isMainNet": false, "isMainNet": false,
"tokens": { "tokens": {
@@ -558,7 +558,7 @@
"ethereumChainId": "5", "ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4", "dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png", "chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api", "squidIntegratorId": "dYdX-api",
"isMainNet": false, "isMainNet": false,
"tokens": { "tokens": {
@@ -637,7 +637,7 @@
"ethereumChainId": "5", "ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4", "dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png", "chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api", "squidIntegratorId": "dYdX-api",
"isMainNet": false, "isMainNet": false,
"tokens": { "tokens": {
@@ -713,7 +713,7 @@
"ethereumChainId": "5", "ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4", "dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png", "chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api", "squidIntegratorId": "dYdX-api",
"isMainNet": false, "isMainNet": false,
"tokens": { "tokens": {
@@ -789,7 +789,7 @@
"ethereumChainId": "5", "ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4", "dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png", "chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api", "squidIntegratorId": "dYdX-api",
"isMainNet": false, "isMainNet": false,
"tokens": { "tokens": {
@@ -865,7 +865,7 @@
"ethereumChainId": "5", "ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4", "dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png", "chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api", "squidIntegratorId": "dYdX-api",
"isMainNet": false, "isMainNet": false,
"tokens": { "tokens": {
@@ -941,7 +941,7 @@
"ethereumChainId": "5", "ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4", "dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png", "chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api", "squidIntegratorId": "dYdX-api",
"isMainNet": false, "isMainNet": false,
"tokens": { "tokens": {
@@ -1008,7 +1008,7 @@
"ethereumChainId": "5", "ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4", "dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png", "chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api", "squidIntegratorId": "dYdX-api",
"isMainNet": false, "isMainNet": false,
"tokens": { "tokens": {
@@ -1084,7 +1084,7 @@
"ethereumChainId": "1", "ethereumChainId": "1",
"dydxChainId": "[mainnet chain id]", "dydxChainId": "[mainnet chain id]",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png", "chainLogo": "/dydx-chain.png",
"squidIntegratorId": "[mainnet squid integrator id]", "squidIntegratorId": "[mainnet squid integrator id]",
"isMainNet": true, "isMainNet": true,
"tokens": { "tokens": {
-4
View File
@@ -22,8 +22,6 @@ import { NotificationsProvider } from '@/hooks/useNotifications';
import { LocalNotificationsProvider } from '@/hooks/useLocalNotifications'; import { LocalNotificationsProvider } from '@/hooks/useLocalNotifications';
import { RestrictionProvider } from '@/hooks/useRestrictions'; import { RestrictionProvider } from '@/hooks/useRestrictions';
import { SubaccountProvider } from '@/hooks/useSubaccount'; import { SubaccountProvider } from '@/hooks/useSubaccount';
import { SquidProvider } from '@/hooks/useSquid';
import { TestFlagsProvider } from '@/hooks/useTestFlags';
import { GuardedMobileRoute } from '@/components/GuardedMobileRoute'; import { GuardedMobileRoute } from '@/components/GuardedMobileRoute';
@@ -124,13 +122,11 @@ const providers = [
wrapProvider(QueryClientProvider, { client: queryClient }), wrapProvider(QueryClientProvider, { client: queryClient }),
wrapProvider(GrazProvider), wrapProvider(GrazProvider),
wrapProvider(WagmiConfig, { config }), wrapProvider(WagmiConfig, { config }),
wrapProvider(TestFlagsProvider),
wrapProvider(LocaleProvider), wrapProvider(LocaleProvider),
wrapProvider(RestrictionProvider), wrapProvider(RestrictionProvider),
wrapProvider(DydxProvider), wrapProvider(DydxProvider),
wrapProvider(AccountsProvider), wrapProvider(AccountsProvider),
wrapProvider(SubaccountProvider), wrapProvider(SubaccountProvider),
wrapProvider(SquidProvider),
wrapProvider(LocalNotificationsProvider), wrapProvider(LocalNotificationsProvider),
wrapProvider(NotificationsProvider), wrapProvider(NotificationsProvider),
wrapProvider(DialogAreaProvider), wrapProvider(DialogAreaProvider),
+1 -1
View File
@@ -61,7 +61,7 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
return ( return (
<StyledBaseButton <StyledBaseButton
disabled={state[ButtonState.Disabled]} disabled={state[ButtonState.Disabled] || state[ButtonState.Loading]}
{...{ ref, action, size, shape, state, ...otherProps }} {...{ ref, action, size, shape, state, ...otherProps }}
> >
{ {
+30 -20
View File
@@ -10,7 +10,7 @@ import {
USD_DECIMALS, USD_DECIMALS,
} from '@/constants/numbers'; } from '@/constants/numbers';
import { BIG_NUMBERS, MustBigNumber } from '@/lib/numbers'; import { BIG_NUMBERS } from '@/lib/numbers';
import { useLocaleSeparators } from '@/hooks'; import { useLocaleSeparators } from '@/hooks';
export enum InputType { export enum InputType {
@@ -29,31 +29,38 @@ type StyleProps = {
type ElementProps = { type ElementProps = {
type?: InputType; type?: InputType;
value?: string | number | null; value?: string | number | null;
allowNegative?: boolean;
decimals?: number;
disabled?: boolean; disabled?: boolean;
id?: string; id?: string;
max?: number;
onBlur?: () => void; onBlur?: () => void;
onChange?:
| Dispatch<SetStateAction<string>>
| React.ReactEventHandler<HTMLInputElement>
| ((values: NumberFormatValues, e: SourceInfo) => void);
onFocus?: () => void; onFocus?: () => void;
onInput?: ({
value,
floatValue,
formattedValue,
}: {
value: string;
floatValue?: number;
formattedValue: string;
}) => void;
placeholder?: string; 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>( export const Input = forwardRef<HTMLInputElement, InputProps>(
( (
@@ -129,6 +136,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
placeholder={placeholder} placeholder={placeholder}
value={value} value={value}
// Other // Other
data-1p-ignore // prevent 1Password fill
{...otherProps} {...otherProps}
/> />
) : ( ) : (
@@ -137,6 +145,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
ref={ref as React.Ref<typeof NumericFormat<unknown>>} ref={ref as React.Ref<typeof NumericFormat<unknown>>}
id={id} id={id}
// NumericFormat // NumericFormat
valueIsNumericString
allowNegative={allowNegative} allowNegative={allowNegative}
decimalScale={decimals} decimalScale={decimals}
decimalSeparator={LOCALE_DECIMAL_SEPARATOR} decimalSeparator={LOCALE_DECIMAL_SEPARATOR}
@@ -160,7 +169,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
? undefined ? undefined
: Number(formattedValue.replace(',', '.')); : Number(formattedValue.replace(',', '.'));
onInput?.({ value, floatValue, formattedValue }); onInput?.({ value, floatValue, formattedValue, ...e });
}} }}
// Native // Native
disabled={disabled} disabled={disabled}
@@ -168,6 +177,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
value={formattedValue} value={formattedValue}
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"
data-1p-ignore // prevent 1Password fill
{...otherProps} {...otherProps}
/> />
)} )}
+15
View File
@@ -46,4 +46,19 @@ export const MARKETS_TO_DISPLAY = [
'SHIB-USD', 'SHIB-USD',
'COMP-USD', 'COMP-USD',
'LDO-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',
]; ];
+1
View File
@@ -129,6 +129,7 @@ export type TransferNotifcation = {
fromChainId?: string; fromChainId?: string;
toAmount?: number; toAmount?: number;
triggeredAt?: number; triggeredAt?: number;
isCctp?: boolean;
errorCount?: number; errorCount?: number;
status?: StatusResponse; status?: StatusResponse;
}; };
+11 -1
View File
@@ -2,7 +2,7 @@ import { useCallback, useContext, createContext, useEffect, useState, useMemo }
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { AES, enc } from 'crypto-js'; 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 { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account';
import { DialogTypes } from '@/constants/dialogs'; import { DialogTypes } from '@/constants/dialogs';
@@ -219,6 +219,16 @@ const useAccountsContext = () => {
else abacusStateManager.attemptDisconnectAccount(); else abacusStateManager.attemptDisconnectAccount();
}, [localDydxWallet]); }, [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 // clear subaccounts when no dydxAddress is set
useEffect(() => { useEffect(() => {
(async () => { (async () => {
+11 -9
View File
@@ -1,11 +1,11 @@
import { createContext, useContext, useCallback, useEffect, useMemo } from 'react'; import { createContext, useContext, useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import type { StatusResponse } from '@0xsquid/sdk';
import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage'; import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage';
import { type TransferNotifcation } from '@/constants/notifications'; import { type TransferNotifcation } from '@/constants/notifications';
import { useAccounts } from '@/hooks/useAccounts'; 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'; import { useLocalStorage } from './useLocalStorage';
@@ -68,8 +68,6 @@ const useLocalNotificationsContext = () => {
[transferNotifications] [transferNotifications]
); );
const squid = useSquid();
useQuery({ useQuery({
queryKey: 'getTransactionStatus', queryKey: 'getTransactionStatus',
queryFn: async () => { queryFn: async () => {
@@ -81,23 +79,27 @@ const useLocalNotificationsContext = () => {
toChainId, toChainId,
fromChainId, fromChainId,
triggeredAt, triggeredAt,
isCctp,
errorCount, errorCount,
status: currentStatus, status: currentStatus,
} = transferNotification; } = 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 ( if (
// @ts-ignore status.errors is not in the type definition but can be returned !hasErrors &&
!currentStatus?.errors &&
!currentStatus?.error &&
(!currentStatus?.squidTransactionStatus || (!currentStatus?.squidTransactionStatus ||
currentStatus?.squidTransactionStatus === 'ongoing') currentStatus?.squidTransactionStatus === 'ongoing')
) { ) {
try { try {
const status = await squid?.getStatus({ const status = await fetchSquidStatus({
transactionId: txHash, transactionId: txHash,
toChainId, toChainId,
fromChainId, fromChainId,
}); }, isCctp);
if (status) { if (status) {
transferNotification.status = status; transferNotification.status = status;
+1 -1
View File
@@ -8,7 +8,7 @@ import {
MARKETS_TO_DISPLAY, MARKETS_TO_DISPLAY,
} from '@/constants/markets'; } from '@/constants/markets';
import { testFlags } from '@/hooks/useTestFlags'; import { testFlags } from '@/lib/testFlags';
import { getAssets } from '@/state/assetsSelectors'; import { getAssets } from '@/state/assetsSelectors';
import { getPerpetualMarkets } from '@/state/perpetualsSelectors'; import { getPerpetualMarkets } from '@/state/perpetualsSelectors';
-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);
-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; const { limitPriceInput, triggerPriceInput, trailingPercentInput } = tradeFormInputValues;
useEffect(() => { useEffect(() => {
const floatValue = parseFloat(triggerPriceInput);
abacusStateManager.setTradeValue({ abacusStateManager.setTradeValue({
value: floatValue, value: triggerPriceInput,
field: TradeInputField.triggerPrice, field: TradeInputField.triggerPrice,
}); });
}, [triggerPriceInput]); }, [triggerPriceInput]);
useEffect(() => { useEffect(() => {
const floatValue = parseFloat(limitPriceInput); abacusStateManager.setTradeValue({ value: limitPriceInput, field: TradeInputField.limitPrice });
abacusStateManager.setTradeValue({ value: floatValue, field: TradeInputField.limitPrice });
}, [limitPriceInput]); }, [limitPriceInput]);
useEffect(() => { useEffect(() => {
const floatValue = parseFloat(trailingPercentInput);
abacusStateManager.setTradeValue({ abacusStateManager.setTradeValue({
value: floatValue, value: trailingPercentInput,
field: TradeInputField.trailingPercent, field: TradeInputField.trailingPercent,
}); });
}, [trailingPercentInput]); }, [trailingPercentInput]);
+68 -1
View File
@@ -1,7 +1,7 @@
import Abacus, { type Nullable } from '@dydxprotocol/v4-abacus'; import Abacus, { type Nullable } from '@dydxprotocol/v4-abacus';
import Long from 'long'; import Long from 'long';
import type { IndexedTx } from '@cosmjs/stargate'; import type { IndexedTx } from '@cosmjs/stargate';
import { encodeJson } from '@dydxprotocol/v4-client-js'; import { GAS_MULTIPLIER, encodeJson } from '@dydxprotocol/v4-client-js';
import { import {
CompositeClient, CompositeClient,
@@ -9,6 +9,7 @@ import {
type LocalWallet, type LocalWallet,
Network, Network,
NetworkOptimizer, NetworkOptimizer,
NobleClient,
SubaccountClient, SubaccountClient,
ValidatorConfig, ValidatorConfig,
OrderType, OrderType,
@@ -43,8 +44,10 @@ import { log } from '../telemetry';
class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
private compositeClient: CompositeClient | undefined; private compositeClient: CompositeClient | undefined;
private nobleClient: NobleClient | undefined;
private store: RootStore | undefined; private store: RootStore | undefined;
private localWallet: LocalWallet | undefined; private localWallet: LocalWallet | undefined;
private nobleWallet: LocalWallet | undefined;
constructor() { constructor() {
this.compositeClient = undefined; this.compositeClient = undefined;
@@ -59,6 +62,13 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
this.localWallet = localWallet; this.localWallet = localWallet;
} }
setNobleWallet(nobleWallet: LocalWallet) {
this.nobleWallet = nobleWallet;
if (this.nobleClient) {
this.nobleClient.connect(nobleWallet);
}
}
async connectNetwork( async connectNetwork(
paramsInJson: Nullable<string>, paramsInJson: Nullable<string>,
callback: (p0: Nullable<string>) => void callback: (p0: Nullable<string>) => void
@@ -70,6 +80,7 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
websocketUrl, websocketUrl,
validatorUrl, validatorUrl,
chainId, chainId,
nobleValidatorUrl,
USDC_DENOM, USDC_DENOM,
USDC_DECIMALS, USDC_DECIMALS,
USDC_GAS_DENOM, USDC_GAS_DENOM,
@@ -101,6 +112,10 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
this.compositeClient = compositeClient; 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 // Dispatch custom event to notify other parts of the app that the network has been connected
const customEvent = new CustomEvent('abacus:connectNetwork', { const customEvent = new CustomEvent('abacus:connectNetwork', {
detail: parsedParams, detail: parsedParams,
@@ -325,6 +340,46 @@ 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 transaction( async transaction(
type: TransactionTypes, type: TransactionTypes,
paramsInJson: Abacus.Nullable<string>, paramsInJson: Abacus.Nullable<string>,
@@ -354,6 +409,11 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
callback(result); callback(result);
break; break;
} }
case TransactionType.SendNobleIBC: {
const result = await this.sendNobleIBC(params);
callback(result);
break;
}
default: { default: {
break; break;
} }
@@ -441,6 +501,13 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
const parseDelegations = this.parseToPrimitives(delegations); const parseDelegations = this.parseToPrimitives(delegations);
callback(JSON.stringify(parseDelegations)); callback(JSON.stringify(parseDelegations));
break; 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: default:
break; break;
} }
+14 -14
View File
@@ -26,13 +26,15 @@ import {
} from '@/constants/abacus'; } from '@/constants/abacus';
import { DEFAULT_MARKETID } from '@/constants/markets'; 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 { CLEARED_SIZE_INPUTS, CLEARED_TRADE_INPUTS } from '@/constants/trade';
import type { RootStore } from '@/state/_store'; import type { RootStore } from '@/state/_store';
import { setTradeFormInputs } from '@/state/inputs'; import { setTradeFormInputs } from '@/state/inputs';
import { getInputTradeOptions, getTransferInputs } from '@/state/inputsSelectors'; import { getInputTradeOptions, getTransferInputs } from '@/state/inputsSelectors';
import { testFlags } from '@/lib/testFlags';
import AbacusRest from './rest'; import AbacusRest from './rest';
import AbacusAnalytics from './analytics'; import AbacusAnalytics from './analytics';
import AbacusWebsocket from './websocket'; import AbacusWebsocket from './websocket';
@@ -81,10 +83,14 @@ class AbacusStateManager {
this.abacusFormatter this.abacusFormatter
); );
const appConfigs = AbacusAppConfig.Companion.forWeb;
if (!isMainnet || testFlags.withCCTP)
appConfigs.squidVersion = AbacusAppConfig.SquidVersion.V2DepositOnly;
this.stateManager = new AsyncAbacusStateManager( this.stateManager = new AsyncAbacusStateManager(
'', '',
CURRENT_ABACUS_DEPLOYMENT, CURRENT_ABACUS_DEPLOYMENT,
AbacusAppConfig.Companion.forWeb, appConfigs,
ioImplementations, ioImplementations,
uiImplementations, uiImplementations,
// @ts-ignore // @ts-ignore
@@ -180,6 +186,12 @@ class AbacusStateManager {
} }
}; };
setNobleWallet = (nobleWallet?: LocalWallet) => {
if (nobleWallet) {
this.chainTransactions.setNobleWallet(nobleWallet);
}
};
setTransfersSourceAddress = (evmAddress: string) => { setTransfersSourceAddress = (evmAddress: string) => {
this.stateManager.sourceAddress = evmAddress; this.stateManager.sourceAddress = evmAddress;
}; };
@@ -223,18 +235,6 @@ class AbacusStateManager {
this.abacusFormatter.setLocaleSeparators({ group, decimal }); this.abacusFormatter.setLocaleSeparators({ group, decimal });
}; };
setTransferStatus = ({
hash,
fromChainId,
toChainId,
}: {
hash: string;
fromChainId?: string;
toChainId?: string;
}) => {
this.stateManager.transferStatus(hash, fromChainId, toChainId);
};
// ------ Transactions ------ // // ------ Transactions ------ //
placeOrder = ( placeOrder = (
+1 -1
View File
@@ -10,7 +10,7 @@ import {
} from '@/constants/websocket'; } from '@/constants/websocket';
import { lastSuccessfulWebsocketRequestByOrigin } from '@/hooks/useAnalytics'; import { lastSuccessfulWebsocketRequestByOrigin } from '@/hooks/useAnalytics';
import { testFlags } from '@/hooks/useTestFlags'; import { testFlags } from '@/lib/testFlags';
import { subscriptionsByChannelId } from '@/lib/tradingView/dydxfeed/cache'; import { subscriptionsByChannelId } from '@/lib/tradingView/dydxfeed/cache';
import { mapCandle } from '@/lib/tradingView/utils'; import { mapCandle } from '@/lib/tradingView/utils';
+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(), inputErrors: errors?.toArray(),
tradeInputs: trade, tradeInputs: trade,
closePositionInputs: closePosition, 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 { STRING_KEYS } from '@/constants/localization';
import { MARKETS_TO_DISPLAY } from '@/constants/markets'; import { MARKETS_TO_DISPLAY } from '@/constants/markets';
import { useBreakpoints, useStringGetter } from '@/hooks'; import { useBreakpoints, useStringGetter } from '@/hooks';
import { testFlags } from '@/hooks/useTestFlags'; import { testFlags } from '@/lib/testFlags';
import { breakpoints } from '@/styles'; import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
@@ -18,7 +18,6 @@ import type { EvmAddress } from '@/constants/wallets';
import { useAccounts, useDebounce, useStringGetter, useSelectedNetwork } from '@/hooks'; import { useAccounts, useDebounce, useStringGetter, useSelectedNetwork } from '@/hooks';
import { useAccountBalance, CHAIN_DEFAULT_TOKEN_ADDRESS } from '@/hooks/useAccountBalance'; import { useAccountBalance, CHAIN_DEFAULT_TOKEN_ADDRESS } from '@/hooks/useAccountBalance';
import { useLocalNotifications } from '@/hooks/useLocalNotifications'; import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { NATIVE_TOKEN_ADDRESS, useSquid } from '@/hooks/useSquid';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins'; import { formMixins } from '@/styles/formMixins';
@@ -38,6 +37,7 @@ import { getTransferInputs } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus'; import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers'; import { MustBigNumber } from '@/lib/numbers';
import { getNobleChainId, NATIVE_TOKEN_ADDRESS } from '@/lib/squid';
import { log } from '@/lib/telemetry'; import { log } from '@/lib/telemetry';
import { parseWalletError } from '@/lib/wallet'; import { parseWalletError } from '@/lib/wallet';
@@ -69,6 +69,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
summary, summary,
errors: routeErrors, errors: routeErrors,
errorMessage: routeErrorMessage, errorMessage: routeErrorMessage,
isCctp,
} = useSelector(getTransferInputs, shallowEqual) || {}; } = useSelector(getTransferInputs, shallowEqual) || {};
const chainId = chainIdStr ? parseInt(chainIdStr) : undefined; const chainId = chainIdStr ? parseInt(chainIdStr) : undefined;
@@ -84,7 +85,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
); );
const [fromAmount, setFromAmount] = useState(''); 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); const debouncedAmount = useDebounce<string>(fromAmount, 500);
// Async Data // Async Data
@@ -99,6 +100,8 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const debouncedAmountBN = MustBigNumber(debouncedAmount); const debouncedAmountBN = MustBigNumber(debouncedAmount);
const balanceBN = MustBigNumber(balance); const balanceBN = MustBigNumber(balance);
useEffect(() => setSlippage(isCctp ? 0 : 0.01), [isCctp]);
useEffect(() => { useEffect(() => {
const hasInvalidInput = const hasInvalidInput =
debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0) || debouncedAmountBN.gt(balanceBN); debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0) || debouncedAmountBN.gt(balanceBN);
@@ -250,11 +253,11 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
if (txHash) { if (txHash) {
addTransferNotification({ addTransferNotification({
txHash: txHash, txHash: txHash,
toChainId: ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId, toChainId: !isCctp ? ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId : getNobleChainId(),
fromChainId: chainIdStr || undefined, fromChainId: chainIdStr || undefined,
toAmount: summary?.usdcSize || undefined, toAmount: summary?.usdcSize || undefined,
triggeredAt: Date.now(), triggeredAt: Date.now(),
notificationStatus: NotificationStatus.Triggered, isCctp,
}); });
abacusStateManager.clearTransferInputValues(); abacusStateManager.clearTransferInputValues();
setFromAmount(''); setFromAmount('');
@@ -109,6 +109,8 @@ export const DepositButtonAndReceipt = ({
? stringGetter({ key: STRING_KEYS.HIDE_ALL_DETAILS }) ? stringGetter({ key: STRING_KEYS.HIDE_ALL_DETAILS })
: stringGetter({ key: STRING_KEYS.SHOW_ALL_DETAILS }); : stringGetter({ key: STRING_KEYS.SHOW_ALL_DETAILS });
const totalFees = (summary?.bridgeFee || 0) + (summary?.gasFee || 0);
const submitButtonReceipt = [ const submitButtonReceipt = [
{ {
key: 'equity', key: 'equity',
@@ -158,9 +160,7 @@ export const DepositButtonAndReceipt = ({
{ {
key: 'total-fees', key: 'total-fees',
label: <span>{stringGetter({ key: STRING_KEYS.TOTAL_FEES })}</span>, label: <span>{stringGetter({ key: STRING_KEYS.TOTAL_FEES })}</span>,
value: typeof summary?.bridgeFee === 'number' && typeof summary?.gasFee === 'number' && ( value: <Output type={OutputType.Fiat} value={totalFees} />,
<Output type={OutputType.Fiat} value={summary?.bridgeFee + summary?.gasFee} />
),
subitems: feeSubitems, subitems: feeSubitems,
}, },
{ {
@@ -183,7 +183,12 @@ export const DepositButtonAndReceipt = ({
type={OutputType.Text} type={OutputType.Text}
value={stringGetter({ value={stringGetter({
key: STRING_KEYS.X_MINUTES_LOWERCASED, key: STRING_KEYS.X_MINUTES_LOWERCASED,
params: { X: Math.round(summary?.estimatedRouteDuration / 60) }, params: {
X:
summary?.estimatedRouteDuration < 60
? '< 1'
: Math.round(summary?.estimatedRouteDuration / 60),
},
})} })}
/> />
), ),
@@ -58,11 +58,6 @@ export const WithdrawForm = () => {
const { sendSquidWithdraw } = useSubaccount(); const { sendSquidWithdraw } = useSubaccount();
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {}; 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 { const {
requestPayload, requestPayload,
token, token,
@@ -71,8 +66,15 @@ export const WithdrawForm = () => {
resources, resources,
errors: routeErrors, errors: routeErrors,
errorMessage: routeErrorMessage, errorMessage: routeErrorMessage,
isCctp
} = useSelector(getTransferInputs, shallowEqual) || {}; } = 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 isValidAddress = toAddress && isAddress(toAddress);
const toToken = useMemo( const toToken = useMemo(
@@ -89,6 +91,8 @@ export const WithdrawForm = () => {
[freeCollateral?.current] [freeCollateral?.current]
); );
useEffect(() => setSlippage(isCctp ? 0 : 0.01), [isCctp]);
useEffect(() => { useEffect(() => {
abacusStateManager.setTransferValue({ abacusStateManager.setTransferValue({
field: TransferInputField.type, field: TransferInputField.type,
@@ -89,14 +89,14 @@ export const WithdrawButtonAndReceipt = ({
const showSubitemsToggle = showFeeBreakdown const showSubitemsToggle = showFeeBreakdown
? stringGetter({ key: STRING_KEYS.HIDE_ALL_DETAILS }) ? stringGetter({ key: STRING_KEYS.HIDE_ALL_DETAILS })
: stringGetter({ key: STRING_KEYS.SHOW_ALL_DETAILS }); : stringGetter({ key: STRING_KEYS.SHOW_ALL_DETAILS });
const totalFees = (summary?.bridgeFee || 0) + (summary?.gasFee || 0);
const submitButtonReceipt = [ const submitButtonReceipt = [
{ {
key: 'total-fees', key: 'total-fees',
label: <span>{stringGetter({ key: STRING_KEYS.TOTAL_FEES })}</span>, label: <span>{stringGetter({ key: STRING_KEYS.TOTAL_FEES })}</span>,
value: typeof summary?.bridgeFee === 'number' && typeof summary?.gasFee === 'number' && ( value: <Output type={OutputType.Fiat} value={totalFees} />,
<Output type={OutputType.Fiat} value={summary?.bridgeFee + summary?.gasFee} />
),
subitems: feeSubitems, subitems: feeSubitems,
}, },
{ {
@@ -165,7 +165,12 @@ export const WithdrawButtonAndReceipt = ({
type={OutputType.Text} type={OutputType.Text}
value={stringGetter({ value={stringGetter({
key: STRING_KEYS.X_MINUTES_LOWERCASED, 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 })); dispatch(setTradeFormInputs({ triggerPriceInput: value }));
}, },
value: triggerPriceInput ?? '', value: triggerPriceInput ?? '',
decimals: tickSizeDecimals || USD_DECIMALS, decimals: tickSizeDecimals ?? USD_DECIMALS,
}); });
} }
@@ -226,7 +226,7 @@ export const TradeForm = ({
dispatch(setTradeFormInputs({ limitPriceInput: value })); dispatch(setTradeFormInputs({ limitPriceInput: value }));
}, },
value: limitPriceInput, value: limitPriceInput,
decimals: tickSizeDecimals || USD_DECIMALS, decimals: tickSizeDecimals ?? USD_DECIMALS,
}); });
} }
+20 -8
View File
@@ -49,7 +49,7 @@ export const TradeSizeInputs = () => {
useSelector(getCurrentMarketConfig, shallowEqual) || {}; useSelector(getCurrentMarketConfig, shallowEqual) || {};
const { size, usdcSize, leverage, input: lastEditedInput } = inputTradeSizeData || {}; const { size, usdcSize, leverage, input: lastEditedInput } = inputTradeSizeData || {};
const { needsLeverage } = currentTradeInputOptions || {}; const { needsLeverage } = currentTradeInputOptions || {};
const decimals = stepSizeDecimals || TOKEN_DECIMALS; const decimals = stepSizeDecimals ?? TOKEN_DECIMALS;
const { amountInput, usdAmountInput, leverageInput } = useSelector( const { amountInput, usdAmountInput, leverageInput } = useSelector(
getTradeFormInputs, getTradeFormInputs,
@@ -70,22 +70,34 @@ export const TradeSizeInputs = () => {
} }
}, [size, usdcSize, leverage, lastEditedInput]); }, [size, usdcSize, leverage, lastEditedInput]);
const onSizeInput = ({ value, floatValue }: { value: string; floatValue?: number }) => { const onSizeInput = ({
dispatch(setTradeFormInputs({ amountInput: value })); floatValue,
formattedValue,
}: {
floatValue?: number;
formattedValue: string;
}) => {
dispatch(setTradeFormInputs({ amountInput: MustBigNumber(floatValue).toString() }));
const newAmount = MustBigNumber(floatValue).toFixed(decimals); const newAmount = MustBigNumber(floatValue).toFixed(decimals);
abacusStateManager.setTradeValue({ abacusStateManager.setTradeValue({
value: value === '' || newAmount === 'NaN' ? null : newAmount, value: formattedValue === '' || newAmount === 'NaN' ? null : newAmount,
field: TradeInputField.size, field: TradeInputField.size,
}); });
}; };
const onUSDCInput = ({ value, floatValue }: { value: string; floatValue?: number }) => { const onUSDCInput = ({
dispatch(setTradeFormInputs({ usdAmountInput: value })); floatValue,
const newUsdcAmount = MustBigNumber(floatValue).toFixed(); formattedValue,
}: {
floatValue?: number;
formattedValue: string;
}) => {
dispatch(setTradeFormInputs({ usdAmountInput: MustBigNumber(floatValue).toString() }));
const newUsdcAmount = MustBigNumber(floatValue).toFixed(tickSizeDecimals || USD_DECIMALS);
abacusStateManager.setTradeValue({ abacusStateManager.setTradeValue({
value: value === '' || newUsdcAmount === 'NaN' ? null : newUsdcAmount, value: formattedValue === '' || newUsdcAmount === 'NaN' ? null : newUsdcAmount,
field: TradeInputField.usdcSize, field: TradeInputField.usdcSize,
}); });
}; };
@@ -44,6 +44,7 @@ export const TransferStatusNotification = ({
// @ts-ignore status.errors is not in the type definition but can be returned // @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 error = status?.errors?.length ? status?.errors[0] : status?.error;
const hasError = error && Object.keys(error).length !== 0;
const updateSecondsLeft = useCallback(() => { const updateSecondsLeft = useCallback(() => {
const fromChainEta = (status?.fromChain?.chainData?.estimatedRouteDuration || 0) * 1000; const fromChainEta = (status?.fromChain?.chainData?.estimatedRouteDuration || 0) * 1000;
@@ -87,7 +88,7 @@ export const TransferStatusNotification = ({
}, },
})} })}
</Styled.Status> </Styled.Status>
{error && ( {hasError && (
<AlertMessage type={AlertType.Error}> <AlertMessage type={AlertType.Error}>
{stringGetter({ {stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE, key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
@@ -112,7 +113,7 @@ export const TransferStatusNotification = ({
) : ( ) : (
<Styled.BridgingStatus> <Styled.BridgingStatus>
{content} {content}
{!isToast && status?.squidTransactionStatus !== 'success' && ( {!isToast && status?.squidTransactionStatus !== 'success' && !hasError && (
<Styled.TransferStatusSteps status={status} type={type} /> <Styled.TransferStatusSteps status={status} type={type} />
)} )}
</Styled.BridgingStatus> </Styled.BridgingStatus>
@@ -120,8 +121,7 @@ export const TransferStatusNotification = ({
} }
slotAction={ slotAction={
isToast && isToast &&
status && status && (
!error && (
<Styled.Trigger <Styled.Trigger
isOpen={open} isOpen={open}
onClick={(e: MouseEvent) => { onClick={(e: MouseEvent) => {
+69 -39
View File
@@ -42,7 +42,11 @@ type StyleProps = {
className?: string; 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 useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number }) => {
const orderbook = useSelector(getCurrentMarketOrderbook, shallowEqual); const orderbook = useSelector(getCurrentMarketOrderbook, shallowEqual);
@@ -53,8 +57,9 @@ const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number
return useMemo(() => { return useMemo(() => {
const asks = (orderbook?.asks?.toArray() ?? []) const asks = (orderbook?.asks?.toArray() ?? [])
.map( .map(
(row: OrderbookLine) => (row: OrderbookLine, idx: number) =>
({ ({
key: `ask-${idx}`,
side: 'ask', side: 'ask',
mine: openOrdersBySideAndPrice[OrderSide.SELL]?.[row.price]?.size, mine: openOrdersBySideAndPrice[OrderSide.SELL]?.[row.price]?.size,
...row, ...row,
@@ -64,8 +69,9 @@ const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number
const bids = (orderbook?.bids?.toArray() ?? []) const bids = (orderbook?.bids?.toArray() ?? [])
.map( .map(
(row: OrderbookLine) => (row: OrderbookLine, idx: number) =>
({ ({
key: `bid-${idx}`,
side: 'bid', side: 'bid',
mine: openOrdersBySideAndPrice[OrderSide.BUY]?.[row.price]?.size, mine: openOrdersBySideAndPrice[OrderSide.BUY]?.[row.price]?.size,
...row, ...row,
@@ -97,15 +103,44 @@ const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number
} }
const spread = 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 spreadPercent = orderbook?.spreadPercent;
const histogramRange = Math.max( const histogramRange = Math.max(
Number(bids[bids.length - 1]?.depth), isNaN(Number(bids[bids.length - 1]?.depth)) ? 0 : Number(bids[bids.length - 1]?.depth),
Number(asks[asks.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 }; return { asks, bids, spread, spreadPercent, histogramRange, hasOrderbook: !!orderbook };
}, [orderbook, openOrdersBySideAndPrice]); }, [orderbook, openOrdersBySideAndPrice]);
}; };
@@ -142,31 +177,33 @@ const OrderbookTable = ({
getCellValue: (row: RowData) => row.size, getCellValue: (row: RowData) => row.size,
label: stringGetter({ key: STRING_KEYS.ORDERBOOK_ORDER_SIZE }), label: stringGetter({ key: STRING_KEYS.ORDERBOOK_ORDER_SIZE }),
tag: symbol, tag: symbol,
renderCell: (row: RowData) => ( renderCell: (row: RowData) =>
<Styled.HistogramOutput row.size > 0 && (
highlightText <Styled.HistogramOutput
type={OutputType.Asset} highlightText
value={row.size} type={OutputType.Asset}
fractionDigits={stepSizeDecimals} value={row.size}
histogramSide={histogramSide === 'left' && 'left'} fractionDigits={stepSizeDecimals}
useGrouping={false} histogramSide={histogramSide === 'left' && 'left'}
/> useGrouping={false}
), />
),
}, },
{ {
columnKey: 'price', columnKey: 'price',
getCellValue: (row: RowData) => row.price, getCellValue: (row: RowData) => row.price,
label: stringGetter({ key: STRING_KEYS.PRICE }), label: stringGetter({ key: STRING_KEYS.PRICE }),
tag: 'USD', tag: 'USD',
renderCell: (row: RowData) => ( renderCell: (row: RowData) =>
<OrderbookTradesOutput row.price > 0 && (
highlightText <OrderbookTradesOutput
type={OutputType.Number} highlightText
value={row.price} type={OutputType.Number}
fractionDigits={tickSizeDecimals} value={row.price}
useGrouping={false} fractionDigits={tickSizeDecimals}
/> useGrouping={false}
), />
),
}, },
{ {
columnKey: 'subaccount-orders', columnKey: 'subaccount-orders',
@@ -194,12 +231,13 @@ const OrderbookTable = ({
label="Orderbook" label="Orderbook"
data={data} data={data}
columns={columns} columns={columns}
getRowKey={(row: RowData) => `${row.side}-${row.price}`} getRowKey={(row: RowData) => row.key}
getRowAttributes={(row: RowData) => ({ getRowAttributes={(row: RowData) => ({
'data-side': row.side, 'data-side': row.side,
style: { style: {
'--histogram-bucket-size': row.size, '--histogram-bucket-size': row.size,
'--histogram-bucket-depth': row.depth, '--histogram-bucket-depth': row.depth,
'--tr-pointerEvents': row.price ? 'auto' : 'none',
}, },
})} })}
onRowAction={onRowAction} onRowAction={onRowAction}
@@ -238,19 +276,10 @@ export const Orderbook = ({
maxRowsPerSide, 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( const data = useMemo(
() => () =>
[ [
...bids.slice(0, showRowsPerSide).reverse(), ...bids.reverse(),
{ {
key: 'spread', key: 'spread',
slotCustomRow: (props) => ( slotCustomRow: (props) => (
@@ -267,9 +296,9 @@ export const Orderbook = ({
</Styled.SpreadTableRow> </Styled.SpreadTableRow>
), ),
} as CustomRowConfig, } as CustomRowConfig,
...asks.slice(0, showRowsPerSide), ...asks,
].reverse(), ].reverse(),
[asks, bids, spread, spreadPercent, showRowsPerSide, isTablet] [asks, bids, spread, spreadPercent, isTablet]
); );
const onRowAction = useCallback( const onRowAction = useCallback(
@@ -325,7 +354,7 @@ export const Orderbook = ({
</Styled.Header> </Styled.Header>
<Styled.SplitOrderbook> <Styled.SplitOrderbook>
<OrderbookTable data={asks} histogramSide="right" {...orderbookTableProps} /> <OrderbookTable data={asks} histogramSide="right" {...orderbookTableProps} />
<OrderbookTable data={bids} histogramSide="left" {...orderbookTableProps} /> <OrderbookTable data={bids.reverse()} histogramSide="left" {...orderbookTableProps} />
</Styled.SplitOrderbook> </Styled.SplitOrderbook>
</Styled.HorizontalLayout> </Styled.HorizontalLayout>
); );
@@ -495,6 +524,7 @@ Styled.OrderbookTable = styled(OrderbookTradesTable)<StyleProps>`
tr { tr {
--histogram-bucket-depth: 0; --histogram-bucket-depth: 0;
pointer-events: var(--tr-pointerEvents);
&[data-side='bid'] { &[data-side='bid'] {
--accent-color: var(--color-positive); --accent-color: var(--color-positive);