Compare commits

...
Author SHA1 Message Date
jaredvu 606c82d940 Bump v4-localization 2023-08-29 16:14:42 -07:00
jaredvu 76100c9857 Fix undefined Symbol 2023-08-29 15:38:15 -07:00
jaredvu 9bbd8d30ed WithdrawForm: Use v4-client-js 2023-08-29 14:32:41 -07:00
jaredvu 7a24e2acfd use v4-client-js 2023-08-29 14:31:44 -07:00
jaredvu 90d2ede5c6 Merge branch 'main' into env-list 2023-08-29 14:30:21 -07:00
Bill e635778438 Deposit/Withdraw with abacus (#3)
* Deposit/Withdraw with abacus

* revert package

* e2e working deposit/withdraws

* deprecate usesquidrouter

* remove unused types

* fix chain select menu

* fix switch network bug

* working switch network deposits

* fix withdraw chain select

* address comments

* address comments

* address comments

* fix clear bug

* use diffarrow

* address nits
2023-08-29 12:11:20 -07:00
jaredvu 4ca6df3d31 @dydxprotocol/v4-client -> @dydxprotocol/v4-client-js 2023-08-29 10:22:24 -07:00
aleka 7a456b33d4 Merge pull request #6 from dydxprotocol/readme-vercel-setup
Add instructions for deploying with vercel in readme
2023-08-29 13:07:21 -04:00
jaredvu 271c98c9bb use NETWORK_ENDPOINTS const 2023-08-29 09:43:26 -07:00
jaredvu c397c0dec9 remove log 2023-08-28 21:35:23 -07:00
jaredvu cc53ac59d6 use ENDPOINTS_PATH to keep abacus/fileSystem consistent 2023-08-28 21:34:44 -07:00
jaredvu 80be9b357f remove console.log 2023-08-28 21:18:30 -07:00
jaredvu cda3e2bb6e nits 2023-08-28 21:16:00 -07:00
jaredvu cc6d61344b remove squidRouter switchNetwork calls, bump abacus 2023-08-28 20:54:19 -07:00
jaredvu 7df6e39e87 Use v4-localization endpoints, replace old usage of CLIENT_NETWORK_CONFIGS 2023-08-28 20:47:18 -07:00
aleka 6d25a072bd Add instructions for deploying with vercel in readme 2023-08-28 17:51:03 -04:00
jaredvu 9d3413b04d Merge branch 'main' into env-list 2023-08-28 08:59:45 -07:00
jaredvu 628041e537 add abacusFileSystem protocol 2023-08-28 08:13:29 -07:00
72 changed files with 1596 additions and 1026 deletions
+22 -3
View File
@@ -4,6 +4,8 @@
- Node.js version 18 and `pnpm` installed on your system
For deploying with Vercel, create an account with [Vercel](https://vercel.com/signup) if you don't have one already.
For deploying to IPFS, choose one of the following:
- **Option 1:** A free [web3.storage](https://web3.storage/) account
@@ -51,7 +53,24 @@ pnpm ladle
This will automatically open your default browser at `http://localhost:61000`.
## Part 3: Deploying to IPFS
## Part 3: Deploying with Vercel
### Step 1: Connect your repository to Vercel
Select "Import Git Repository" from your dashboard, and provide the URL of this repository or your forked repository.
### Step 2: Configure your project
For the "Build & Development Settings", we recommend the following:
- Framework Preset: `Vite`
- Build Command (override): `pnpm run build`
If you wish to incorporate analytics via Amplitude and Bugsnag, you can use our scripts:
`pnpm run build:inject-amplitude` and `pnpm run build:inject-bugsnag`. You will need to provide your own API keys for these services. In the Environment Variables section, name the variables as `AMPLITUDE_API_KEY` and `BUGSNAG_API_KEY` and provide the respective keys as their values.
For more details, check out Vercel's [official documentation](https://vercel.com/docs).
## Part 4: Deploying to IPFS
### web3.storage: deploy to IPFS via web3.storage using the provided script
@@ -74,7 +93,7 @@ ipfs add -r dist
Save the CID provided in the output.
## Part 4: Accessing your content on IPFS
### Accessing your content on IPFS
To access your content on IPFS:
@@ -82,4 +101,4 @@ To access your content on IPFS:
2. **Public IPFS gateway:** Access your content via a public IPFS gateway, such as [https://dweb.link](https://dweb.link/) or [https://w3s.link/](https://w3s.link/). Use the gateway URL with your CID appended, like `https://dweb.link/ipfs/your_cid`.
Replace `your_cid` with the actual CID.
Replace `your_cid` with the actual CID.
+3 -2
View File
@@ -35,9 +35,10 @@
"@cosmjs/encoding": "^0.31.0",
"@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0",
"@dydxprotocol/abacus": "^0.2.37",
"@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/v4-client": "^0.29.0",
"@dydxprotocol/abacus": "^0.3.10",
"@dydxprotocol/v4-client-js": "^0.30.3",
"@dydxprotocol/v4-localization": "^0.0.16",
"@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3",
"@radix-ui/react-collapsible": "^1.0.3",
+20 -13
View File
@@ -1,5 +1,9 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
'@0xsquid/sdk':
specifier: ^1.7.2
@@ -23,11 +27,14 @@ dependencies:
specifier: ^0.31.0
version: 0.31.0
'@dydxprotocol/abacus':
specifier: ^0.2.37
version: 0.2.37
'@dydxprotocol/v4-client':
specifier: ^0.29.0
version: 0.29.0
specifier: ^0.3.10
version: 0.3.10
'@dydxprotocol/v4-client-js':
specifier: ^0.30.3
version: 0.30.3
'@dydxprotocol/v4-localization':
specifier: ^0.0.16
version: 0.0.16
'@ethersproject/providers':
specifier: ^5.7.2
version: 5.7.2
@@ -1139,8 +1146,8 @@ packages:
resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==}
dev: true
/@dydxprotocol/abacus@0.2.37:
resolution: {integrity: sha512-amoq9aVXo+lVTQGu2jsyy5/0bniOMEXpyJDmaAr2TpbrtQtw2WgyAP+se9Q5wt3yh0Mzdtsh7P2h090kH7/bZQ==}
/@dydxprotocol/abacus@0.3.10:
resolution: {integrity: sha512-CJV6zyoQLAoDWJdgLbP1Y2QYvSPybCGpykQY+WEWk4kQKnZkjxz25LPL0b8I9LrNDt2JknOxPJfAoUDXJPfNkg==}
dev: false
/@dydxprotocol/dydxjs@0.3.0:
@@ -1160,8 +1167,8 @@ packages:
- utf-8-validate
dev: false
/@dydxprotocol/v4-client@0.29.0:
resolution: {integrity: sha512-BMgm6e91LFGKB0l3lC2/0aIgayejgBa7qCwCUxr05265tggZsI3l3Cui8LNDHeb8IAQ4KPcfAJ29c96lx+rLlw==}
/@dydxprotocol/v4-client-js@0.30.3:
resolution: {integrity: sha512-h9nB2cU+6sWwfOTQz6sMKZZxVa/Iwp1JciUqKi5HDUyWl/4wfbczDlUqbMUUeXgBgfbb9CImxn+PjF0v7lcD5A==}
dependencies:
'@cosmjs/amino': 0.30.1
'@cosmjs/proto-signing': 0.30.1
@@ -1183,6 +1190,10 @@ packages:
- utf-8-validate
dev: false
/@dydxprotocol/v4-localization@0.0.16:
resolution: {integrity: sha512-IuTjuVdGwV6nulBLCxbNydsEkwvx7mBn8SQtPP2pQ16yfUNWMyBG3GDEXlfAk41NcPSCGXlK5Wbl0R1xaOKCRA==}
dev: false
/@emotion/is-prop-valid@1.2.1:
resolution: {integrity: sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==}
dependencies:
@@ -15851,7 +15862,3 @@ packages:
release-it: 15.11.0
semver: 7.5.1
dev: false
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+5 -1
View File
@@ -14,6 +14,8 @@ import { DialogAreaProvider, useDialogArea } from './hooks/useDialogArea';
import { LocaleProvider } from './hooks/useLocaleSeparators';
import { NotificationsProvider } from './hooks/useNotifications';
import { SubaccountProvider } from './hooks/useSubaccount';
import { SquidProvider } from '@/hooks/useSquidRouter';
import { GuardedMobileRoute } from '@/components/GuardedMobileRoute';
import MarketsPage from '@/pages/markets/Markets';
@@ -106,7 +108,9 @@ const App = () => (
<SubaccountProvider>
<NotificationsProvider>
<DialogAreaProvider>
<Content />
<SquidProvider>
<Content />
</SquidProvider>
</DialogAreaProvider>
</NotificationsProvider>
</SubaccountProvider>
+1 -1
View File
@@ -1,5 +1,5 @@
import type { Story } from '@ladle/react';
import { OrderSide } from '@dydxprotocol/v4-client';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { OrderSideTag } from '@/components/OrderSideTag';
+1 -1
View File
@@ -1,4 +1,4 @@
import { OrderSide } from '@dydxprotocol/v4-client';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { STRING_KEYS } from '@/constants/localization';
import { useStringGetter } from '@/hooks';
+8 -7
View File
@@ -19,6 +19,7 @@ type ElementProps = {
disabled?: boolean;
label?: string;
items: MenuConfig<string, string>;
slotEmpty?: ReactNode;
withSearch?: boolean;
withReceiptItems?: DetailsItem[];
};
@@ -52,12 +53,10 @@ export const SearchSelectMenu = ({
const Trigger = asChild ? (
children
) : (
<>
<Styled.MenuTrigger>
<Styled.WithLabel label={label}>{children}</Styled.WithLabel>
</Styled.MenuTrigger>
<Styled.MenuTrigger>
{label ? <Styled.WithLabel label={label}>{children}</Styled.WithLabel> : children}
<Styled.TriggerIcon iconName={IconName.Triangle} open={open} />
</>
</Styled.MenuTrigger>
);
return (
@@ -86,7 +85,7 @@ export const SearchSelectMenu = ({
const Styled: Record<string, AnyStyledComponent> = {};
Styled.MenuTrigger = styled.div`
height: 3.5rem;
height: var(--form-input-height);
${layoutMixins.spacedRow}
align-items: center;
@@ -117,7 +116,7 @@ Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
Styled.Popover = styled(Popover)`
max-height: 30vh;
margin-top: 1rem;
border: 1px solid var(--color-layer-6);
border: var(--border-width) solid var(--color-layer-6);
border-radius: 0.5rem;
z-index: 2;
box-shadow: none;
@@ -136,6 +135,8 @@ Styled.ComboboxMenu = styled(ComboboxMenu)`
--comboboxMenu-item-checked-textColor: var(--color-text-2);
--comboboxMenu-item-highlighted-textColor: var(--color-text-2);
--stickyArea1-topHeight: var(--form-input-height);
input:focus {
outline: none;
}
+13 -1
View File
@@ -1,5 +1,5 @@
import Abacus, { kollections } from '@dydxprotocol/abacus';
import { OrderSide } from '@dydxprotocol/v4-client';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { PositionSide, TradeTypes } from './trade';
import { STRING_KEYS } from './localization';
@@ -36,6 +36,12 @@ export type AbacusThreadingProtocol = Omit<
Abacus.exchange.dydx.abacus.protocols.ThreadingProtocol,
'__doNotUseOrImplementIt'
>;
export type AbacusFileSystemProtocol = Omit<
Abacus.exchange.dydx.abacus.protocols.FileSystemProtocol,
'__doNotUseOrImplementIt'
>;
export type FileLocation = Abacus.exchange.dydx.abacus.protocols.FileLocation;
export type ThreadingType = Abacus.exchange.dydx.abacus.protocols.ThreadingType;
export const CoroutineTimer = Abacus.exchange.dydx.abacus.utils.CoroutineTimer;
@@ -98,6 +104,9 @@ export type ClosePositionInputs = Abacus.exchange.dydx.abacus.output.input.Close
export type TradeInputSummary = Abacus.exchange.dydx.abacus.output.input.TradeInputSummary;
export type TransferInputs = Abacus.exchange.dydx.abacus.output.input.TransferInput;
export type InputError = Abacus.exchange.dydx.abacus.output.input.ValidationError;
export type TransferInputTokenResource = Abacus.exchange.dydx.abacus.output.input.TransferInputTokenResource;
export type TransferInputChainResource = Abacus.exchange.dydx.abacus.output.input.TransferInputChainResource;
export type SelectionOption = Abacus.exchange.dydx.abacus.output.input.SelectionOption;
export const ErrorType = Abacus.exchange.dydx.abacus.output.input.ErrorType;
export const InputSelectionOption = Abacus.exchange.dydx.abacus.output.input.SelectionOption;
@@ -177,6 +186,9 @@ export const RiskLevel = Abacus.exchange.dydx.abacus.utils.RiskLevel;
const riskLevels = [...RiskLevel.values()] as const;
export type RiskLevels = (typeof riskLevels)[number];
// ------ Notifications ------ //
export type AbacusNotification = Abacus.exchange.dydx.abacus.output.Notification;
// ------ Enum Conversions ------ //
type IfEquals<X, Y, A, B> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
? A
+1
View File
@@ -12,6 +12,7 @@ export enum DialogTypes {
Receive = 'Receive',
Trade = 'Trade',
Transfer = 'Transfer',
Withdraw = 'Withdraw',
}
export enum TradeBoxDialogTypes {
+2
View File
@@ -474,6 +474,7 @@ export const APP_STRING_KEYS = {
LEAVING_WEBSITE: 'LEGAL.LEAVING_WEBSITE',
// Deposit Modal
BRIDGE_FEE: 'DEPOSIT_MODAL.BRIDGE_FEE',
ENABLE_FOR_THIS_DEPOSIT: 'DEPOSIT_MODAL.ENABLE_FOR_THIS_DEPOSIT',
ENABLE_SYMBOL_ON_DYDX: 'DEPOSIT_MODAL.ENABLE_SYMBOL_ON_DYDX',
ENABLE_SYMBOL: 'DEPOSIT_MODAL.ENABLE_SYMBOL',
@@ -930,6 +931,7 @@ export const APP_STRING_KEYS = {
SLOW_WITHDRAW_PENDING: 'NOTIFICATIONS.SLOW_WITHDRAW_PENDING',
SUSPICIOUS_TRADE: 'NOTIFICATIONS.SUSPICIOUS_TRADE',
SUSPICIOUS_TRADE_BODY: 'NOTIFICATIONS.SUSPICIOUS_TRADE_BODY',
WITHDRAW_IN_PROGRESS: 'NOTIFICATIONS.WITHDRAW_IN_PROGRESS',
// Email Notifications
ACCOUNT_DESCRIPTION: 'EMAIL_NOTIFICATIONS.ACCOUNT_DESCRIPTION',
+3
View File
@@ -78,6 +78,9 @@ export const ERROR_STRING_KEYS = {
RESTRICTED_OPEN_POSITIONS_WITHDRAW: 'ERRORS.WITHDRAW_MODAL.RESTRICTED_OPEN_POSITIONS_WITHDRAW',
WITHDRAW_MORE_THAN_FREE_DUE_TO_FEE: 'ERRORS.WITHDRAW_MODAL.WITHDRAW_MORE_THAN_FREE_DUE_TO_FEE',
WITHDRAW_MORE_THAN_FREE: 'ERRORS.WITHDRAW_MODAL.WITHDRAW_MORE_THAN_FREE',
WITHDRAW_MUST_SPECIFY_ADDRESS: 'ERRORS.DEPOSIT_MODAL.WITHDRAW_MUST_SPECIFY_ADDRESS',
WITHDRAW_MUST_SPECIFY_ASSET: 'ERRORS.DEPOSIT_MODAL.WITHDRAW_MUST_SPECIFY_ASSET',
WITHDRAW_MUST_SPECIFY_CHAIN: 'ERRORS.DEPOSIT_MODAL.WITHDRAW_MUST_SPECIFY_CHAIN',
// Transfer modal errors
TRANSFER_INVALID_ETH_ADDRESS: 'ERRORS.TRANSFER_MODAL.TRANSFER_INVALID_ETH_ADDRESS',
+18 -62
View File
@@ -1,73 +1,29 @@
import { ENDPOINTS, DEV_ENDPOINTS } from '@dydxprotocol/v4-localization';
export const NETWORK_ENDPOINTS = import.meta.env.MODE === 'production' ? ENDPOINTS : DEV_ENDPOINTS;
export const CLIENT_NETWORK_CONFIGS: Record<
string,
(typeof NETWORK_ENDPOINTS.environments)[number]
> = Object.fromEntries(
NETWORK_ENDPOINTS.environments.map((environment) => [environment.environment, environment])
);
export const DEFAULT_APP_ENVIRONMENT = NETWORK_ENDPOINTS.defaultEnvironment;
export enum DydxV4Network {
V4Mainnet = 'dydxprotocol-mainnet',
V4Staging = 'dydxprotocol-staging',
V4Testnet2 = 'dydxprotocol-testnet-2',
V4Testnet2 = 'dydxprotocol-testnet',
V4Local = 'dydxprotocol-dev',
}
export enum DydxV3Network {
V3Mainnet = '1',
V3Testnet = '5',
}
export type DydxNetwork = keyof typeof CLIENT_NETWORK_CONFIGS;
export function isDydxV4Network(network: any): network is DydxV4Network {
return Object.values(DydxV4Network).includes(network);
export function isDydxV4Network(network: any): boolean {
return CLIENT_NETWORK_CONFIGS[network]?.version === 'v4';
}
export function isValidDydxNetwork(network: any): boolean {
return isDydxV4Network(network) || Object.values(DydxV3Network).includes(network);
return Object.keys(CLIENT_NETWORK_CONFIGS).includes(network);
}
export type DydxNetwork = DydxV4Network | DydxV3Network;
export const CHAIN_IDS = {
[DydxV4Network.V4Mainnet]: '',
[DydxV4Network.V4Staging]: 'dydxprotocol-testnet',
[DydxV4Network.V4Testnet2]: 'dydx-testnet-2',
[DydxV4Network.V4Local]: 'dydxprotocol-testnet',
[DydxV3Network.V3Mainnet]: '1',
[DydxV3Network.V3Testnet]: '5',
};
export const DEFAULT_APP_ENVIRONMENT =
import.meta.env.MODE === 'production' ? DydxV4Network.V4Testnet2 : DydxV4Network.V4Staging;
export const CLIENT_NETWORK_CONFIGS: Record<
DydxV4Network,
{
chainId: string;
validatorUrl: string;
faucetUrl: string;
indexerUrl: string;
ethereumChainId: number;
}
> = {
[DydxV4Network.V4Mainnet]: {
chainId: CHAIN_IDS[DydxV4Network.V4Mainnet],
validatorUrl: import.meta.env.VITE_VALIDATOR_URL_STAGE,
faucetUrl: import.meta.env.VITE_FAUCET_URL_STAGE,
indexerUrl: import.meta.env.VITE_INDEXER_URL_STAGE,
ethereumChainId: 1,
},
[DydxV4Network.V4Local]: {
chainId: CHAIN_IDS[DydxV4Network.V4Local],
validatorUrl: import.meta.env.VITE_VALIDATOR_URL_DEV,
faucetUrl: import.meta.env.VITE_FAUCET_URL_DEV,
indexerUrl: import.meta.env.VITE_INDEXER_URL_DEV,
ethereumChainId: 5,
},
[DydxV4Network.V4Staging]: {
chainId: CHAIN_IDS[DydxV4Network.V4Staging],
validatorUrl: import.meta.env.VITE_VALIDATOR_URL_STAGE,
faucetUrl: import.meta.env.VITE_FAUCET_URL_STAGE,
indexerUrl: import.meta.env.VITE_INDEXER_URL_STAGE,
ethereumChainId: 5,
},
[DydxV4Network.V4Testnet2]: {
chainId: CHAIN_IDS[DydxV4Network.V4Testnet2],
validatorUrl: import.meta.env.VITE_VALIDATOR_URL_TESTNET2,
faucetUrl: import.meta.env.VITE_FAUCET_URL_TESTNET2,
indexerUrl: import.meta.env.VITE_INDEXER_URL_TESTNET2,
ethereumChainId: 5,
},
};
+1 -1
View File
@@ -1,4 +1,4 @@
import { OrderSide } from '@dydxprotocol/v4-client';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { AlertType } from '@/constants/alerts';
import { STRING_KEYS } from '@/constants/localization';
+1 -1
View File
@@ -302,7 +302,7 @@ export type WalletConnection = {
// dYdX Chain wallets
import { USDC_DENOM, type Onboarding, DYDX_DENOM } from '@dydxprotocol/v4-client';
import { USDC_DENOM, type Onboarding, DYDX_DENOM } from '@dydxprotocol/v4-client-js';
import type { suggestChain } from 'graz';
export const COSMOS_DERIVATION_PATH = "m/44'/118'/0'/0/0";
+2
View File
@@ -2,6 +2,7 @@ import { useApiState } from './useApiState';
import { useBreakpoints } from './useBreakpoints';
import { useCommandMenu } from './useCommandMenu';
import { useCurrentMarketId } from './useCurrentMarketId';
import { useDebounce } from './useDebounce';
import { useInterval } from './useInterval';
import { useDocumentTitle } from './useDocumentTitle';
import { useDydxClient } from './useDydxClient';
@@ -25,6 +26,7 @@ export {
useBreakpoints,
useCommandMenu,
useCurrentMarketId,
useDebounce,
useDocumentTitle,
useDydxClient,
useAccountBalance,
+2 -6
View File
@@ -1,6 +1,5 @@
import { useEffect } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import isEmpty from 'lodash/isEmpty';
import { LanguageCode, ResolutionString, widget } from 'public/tradingview/charting_library';
@@ -14,9 +13,8 @@ import { store } from '@/state/_store';
import { getSelectedNetwork } from '@/state/appSelectors';
import { getAppTheme } from '@/state/configsSelectors';
import { getSelectedLocale } from '@/state/localizationSelectors';
import { getMarketIds } from '@/state/perpetualsSelectors';
import { getCurrentMarketId, getMarketIds } from '@/state/perpetualsSelectors';
import { getMarketIdFromLocation } from '@/lib/tradeData';
import { getDydxDatafeed } from '@/lib/tradingView/dydxfeed';
import { getSavedResolution, getWidgetOptions, getWidgetOverrides } from '@/lib/tradingView/utils';
@@ -30,8 +28,7 @@ export const useTradingView = ({
tvWidgetRef: React.MutableRefObject<any>;
setIsChartReady: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
const location = useLocation();
const marketId = getMarketIdFromLocation(location);
const marketId = useSelector(getCurrentMarketId);
const appTheme = useSelector(getAppTheme);
const marketIds = useSelector(getMarketIds, shallowEqual);
const selectedLocale = useSelector(getSelectedLocale);
@@ -50,7 +47,6 @@ export const useTradingView = ({
if (hasMarkets) {
const widgetOptions = getWidgetOptions();
const widgetOverrides = getWidgetOverrides(appTheme);
const options = {
// debug: true,
...widgetOptions,
+8 -22
View File
@@ -3,9 +3,9 @@ import { useSelector } from 'react-redux';
import { useBalance } from 'wagmi';
import { StargateClient } from '@cosmjs/stargate';
import { useQuery } from 'react-query';
import { formatUnits } from 'ethers';
import { formatUnits } from 'viem';
import { CLIENT_NETWORK_CONFIGS, type DydxV4Network } from '@/constants/networks';
import { CLIENT_NETWORK_CONFIGS } from '@/constants/networks';
import { QUANTUM_MULTIPLIER } from '@/constants/numbers';
import { EthereumAddress } from '@/constants/wallets';
@@ -15,7 +15,6 @@ import { MustBigNumber } from '@/lib/numbers';
import { getSelectedNetwork } from '@/state/appSelectors';
import { useAccounts } from './useAccounts';
// import { useDydxClient } from './useDydxClient';
import { usePollNativeTokenBalance } from './usePollNativeTokenBalance';
type UseAccountBalanceProps = {
@@ -43,16 +42,14 @@ export const useAccountBalance = ({
assetSymbol,
bech32AddrPrefix,
chainId,
decimals,
decimals = 0,
rpc,
isCosmosChain,
}: UseAccountBalanceProps = {}) => {
const { evmAddress, dydxAddress } = useAccounts();
// TODO: let Abacus / useDydxClient handle EVM chain IDs
// const { networkConfig } = useDydxClient();
const selectedNetwork = useSelector(getSelectedNetwork);
const evmChainId = Number(CLIENT_NETWORK_CONFIGS[selectedNetwork as DydxV4Network].ethereumChainId);
const evmChainId = Number(CLIENT_NETWORK_CONFIGS[selectedNetwork].ethereumChainId);
const evmQuery = useBalance({
enabled: Boolean(!isCosmosChain && addressOrDenom?.startsWith('0x')),
@@ -66,7 +63,7 @@ export const useAccountBalance = ({
});
const cosmosQueryFn = useCallback(async () => {
if (dydxAddress && bech32AddrPrefix && rpc) {
if (dydxAddress && bech32AddrPrefix && rpc && addressOrDenom) {
const address = convertBech32Address({
address: dydxAddress,
bech32Prefix: bech32AddrPrefix,
@@ -76,14 +73,7 @@ export const useAccountBalance = ({
const balanceAsCoin = await client.getBalance(address, addressOrDenom);
await client.disconnect();
/**
* Hardcode decimals because most cosmos coins default to 6 decimals.
* Squids testnet has some coins as having 18 decimals instead of 6.
* @url https://github1s.com/cosmos/cosmjs/blob/HEAD/packages/amino/src/coins.ts#L8-L16
*
* @todo: change '6' to 'decimals' when Squid testnet data is updated.
* */
return formatUnits(balanceAsCoin.amount, decimals);
return formatUnits(BigInt(balanceAsCoin.amount), decimals);
}
}, [addressOrDenom, chainId, rpc]);
@@ -109,11 +99,7 @@ export const useAccountBalance = ({
return {
balance,
nativeTokenBalance,
isBalanceError: isCosmosChain
? cosmosQuery.status === 'error' && !cosmosQuery.isFetching
: evmQuery.status === 'error' && evmQuery.fetchStatus === 'idle',
isBalanceLoading: isCosmosChain
? cosmosQuery.status === 'loading' && cosmosQuery.isFetching
: evmQuery.status === 'loading' && evmQuery.fetchStatus === 'fetching',
queryStatus: isCosmosChain ? cosmosQuery.status : evmQuery.status,
isQueryFetching: isCosmosChain ? cosmosQuery.isFetching : evmQuery.fetchStatus === 'fetching',
};
};
+4 -5
View File
@@ -2,9 +2,8 @@ import { useCallback, useContext, createContext, useEffect, useState, useMemo }
import { useDispatch } from 'react-redux';
import { AES, enc } from 'crypto-js';
import { LocalWallet, USDC_DENOM, type Subaccount } from '@dydxprotocol/v4-client';
import { LocalWallet, USDC_DENOM, type Subaccount } from '@dydxprotocol/v4-client-js';
import { SubAccountHistoricalPNLs } from '@/constants/abacus';
import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account';
import { LocalStorageKey, LOCAL_STORAGE_VERSIONS } from '@/constants/localStorage';
import { DydxAddress, EthereumAddress, PrivateInformation } from '@/constants/wallets';
@@ -12,8 +11,6 @@ import { DydxAddress, EthereumAddress, PrivateInformation } from '@/constants/wa
import {
setOnboardingState,
setOnboardingGuard,
setSubaccount,
setHistoricalPnl,
} from '@/state/account';
import abacusStateManager from '@/lib/abacus';
@@ -63,6 +60,8 @@ const useAccountsContext = () => {
forgetEvmSignature(previousEvmAddress);
}
if (evmAddress) abacusStateManager.setTransfersSourceAddress(evmAddress);
setPreviousEvmAddress(evmAddress);
}, [evmAddress]);
@@ -224,7 +223,7 @@ const useAccountsContext = () => {
// TODO: useAbacus({ dydxAddress })
useEffect(() => {
if (dydxAddress) abacusStateManager.setAccount(dydxAddress);
else abacusStateManager.disconnectAccount();
else abacusStateManager.attemptDisconnectAccount();
}, [dydxAddress]);
// clear subaccounts when no dydxAddress is set
+1 -1
View File
@@ -29,7 +29,7 @@ export const useCurrentMarketId = () => {
const validId = useMemo(() => {
if (marketIds.length === 0) return marketId ?? lastViewedMarket;
if (!marketIds.includes(marketId)) return DEFAULT_MARKETID;
if (!marketIds.includes(marketId ?? lastViewedMarket)) return DEFAULT_MARKETID;
return marketId ?? lastViewedMarket;
}, [marketIds, marketId]);
+21
View File
@@ -0,0 +1,21 @@
import { useEffect, useState } from 'react';
/**
* https://usehooks-ts.com/react-hook/use-debounce
* @param value
* @param delay
* @returns the debouncedValue
*/
export function useDebounce<T>(value: T, delay?: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
+2 -4
View File
@@ -1,4 +1,4 @@
import { DydxV4Network } from '@/constants/networks';
import { isDydxV4Network } from '@/constants/networks';
import { WalletType } from '@/constants/wallets';
import { isTruthy } from '@/lib/isTruthy';
@@ -11,9 +11,7 @@ export const useDisplayedWallets = () => {
return [
WalletType.MetaMask,
import.meta.env.MODE !== 'production' &&
[DydxV4Network.V4Testnet2, DydxV4Network.V4Staging].includes(selectedNetwork) &&
WalletType.Keplr,
import.meta.env.MODE !== 'production' && isDydxV4Network(selectedNetwork) && WalletType.Keplr,
WalletType.WalletConnect2,
WalletType.WalletConnect,
+1 -1
View File
@@ -9,7 +9,7 @@ import {
onboarding,
Network,
ValidatorConfig,
} from '@dydxprotocol/v4-client';
} from '@dydxprotocol/v4-client-js';
import type { ResolutionString } from 'public/tradingview/charting_library';
+1 -12
View File
@@ -3,12 +3,7 @@ import { useDispatch } from 'react-redux';
import { LocalStorageKey } from '@/constants/localStorage';
import {
DEFAULT_APP_ENVIRONMENT,
DydxNetwork,
isDydxV4Network,
isValidDydxNetwork,
} from '@/constants/networks';
import { DEFAULT_APP_ENVIRONMENT, DydxNetwork, isValidDydxNetwork } from '@/constants/networks';
import { useLocalStorage } from '@/hooks';
@@ -16,8 +11,6 @@ import { initializeLocalization } from '@/state/app';
import abacusStateManager from '@/lib/abacus';
import squidRouter from '@/lib/squidRouter';
export const useInitializePage = () => {
const dispatch = useDispatch();
@@ -31,9 +24,5 @@ export const useInitializePage = () => {
useEffect(() => {
dispatch(initializeLocalization());
abacusStateManager.start({ network: localStorageNetwork });
if (isDydxV4Network(localStorageNetwork)) {
squidRouter.switchNetwork(localStorageNetwork);
}
}, []);
};
+1 -1
View File
@@ -3,7 +3,7 @@ import { useQuery } from 'react-query';
import { useAccounts } from '@/hooks';
import { DydxAddress } from '@/constants/wallets';
import { DYDX_DENOM } from '@dydxprotocol/v4-client';
import { DYDX_DENOM } from '@dydxprotocol/v4-client-js';
const ACCOUNT_BALANCE_POLLING_INTERVAL = 60_000;
+2 -2
View File
@@ -1,8 +1,8 @@
import { useCallback, useEffect } from 'react';
import { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { LocalStorageKey } from '@/constants/localStorage';
import { DEFAULT_APP_ENVIRONMENT, DydxNetwork, DydxV4Network } from '@/constants/networks';
import { DEFAULT_APP_ENVIRONMENT, DydxNetwork } from '@/constants/networks';
import { useAccounts, useLocalStorage } from '@/hooks';
+52
View File
@@ -0,0 +1,52 @@
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { Squid } from '@0xsquid/sdk';
import { DydxV4Network, isDydxV4Network } from '@/constants/networks';
import { getSelectedNetwork } from '@/state/appSelectors';
const SQUID_BASE_URL: Record<DydxV4Network, string | undefined> = {
[DydxV4Network.V4Testnet2]: 'https://squid-api-git-feat-cosmos-maintestnet-0xsquid.vercel.app',
[DydxV4Network.V4Staging]: undefined,
[DydxV4Network.V4Local]: undefined,
[DydxV4Network.V4Mainnet]: 'https://api.0xsquid.com',
};
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(
() =>
isDydxV4Network(selectedNetwork)
? new Squid({ baseUrl: SQUID_BASE_URL[selectedNetwork as DydxV4Network] })
: undefined,
[selectedNetwork]
);
useEffect(() => {
if (squid) {
initializeClient();
}
}, [squid]);
return squid;
};
type SquidContextType = ReturnType<typeof useSquidContext>;
const SquidContext = createContext<SquidContextType>(undefined);
SquidContext.displayName = '0xSquid';
export const SquidProvider = ({ ...props }) => (
<SquidContext.Provider value={useSquidContext()} {...props} />
);
export const useSquid = () => useContext(SquidContext);
+53 -2
View File
@@ -3,7 +3,7 @@ import { useDispatch } from 'react-redux';
import type { Nullable } from '@dydxprotocol/abacus';
import Long from 'long';
import type { IndexedTx } from '@cosmjs/stargate';
import { type Coin } from '@cosmjs/proto-signing';
import type { EncodeObject, Coin } from '@cosmjs/proto-signing';
import { Method } from '@cosmjs/tendermint-rpc';
import {
@@ -17,7 +17,7 @@ import {
DYDX_DENOM,
USDC_DENOM,
GAS_PRICE_DYDX_DENOM,
} from '@dydxprotocol/v4-client';
} from '@dydxprotocol/v4-client-js';
import type {
HumanReadableCancelOrderPayload,
@@ -89,6 +89,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
simulateTransferNativeToken,
placeOrderForSubaccount,
cancelOrderForSubaccount,
sendSquidWithdrawFromSubaccount,
} = useMemo(
() => ({
depositToSubaccount: async ({
@@ -301,6 +302,32 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
return result;
},
sendSquidWithdrawFromSubaccount: async ({
subaccountClient,
amount,
payload,
}: {
subaccountClient: SubaccountClient;
amount: number;
payload: string;
}) => {
if (!compositeClient) throw new Error('client not initialized');
const transaction = JSON.parse(payload);
const msg = compositeClient.withdrawFromSubaccountMessage(subaccountClient, amount);
const ibcMsg: EncodeObject = {
typeUrl: transaction.msgTypeUrl,
value: transaction.msg,
};
return await compositeClient.send(
subaccountClient.wallet,
() => Promise.resolve([msg, ibcMsg]),
false
);
},
}),
[compositeClient]
);
@@ -398,6 +425,28 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
[subaccountClient, simulateWithdrawFromSubaccount, simulateTransferNativeToken]
);
const simulateWithdraw = useCallback(
async (amount: number) => {
if (!subaccountClient) {
return;
}
return await simulateWithdrawFromSubaccount({ subaccountClient, amount });
},
[subaccountClient, simulateWithdrawFromSubaccount]
);
const sendSquidWithdraw = useCallback(
async (amount: number, payload: string) => {
if (!subaccountClient) {
return;
}
return await sendSquidWithdrawFromSubaccount({ subaccountClient, amount, payload });
},
[subaccountClient, sendSquidWithdrawFromSubaccount]
);
// ------ Faucet Methods ------ //
const requestFaucetFunds = useCallback(async () => {
if (!dydxAddress) return;
@@ -589,6 +638,8 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
// Transfer Methods
simulateTransfer,
transfer,
simulateWithdraw,
sendSquidWithdraw,
// Trading Methods
placeOrder,
+2
View File
@@ -17,6 +17,7 @@ import { OnboardingDialog } from '@/views/dialogs/OnboardingDialog';
import { ReceiveDialog } from '@/views/dialogs/ReceiveDialog';
import { TradeDialog } from '@/views/dialogs/TradeDialog';
import { TransferDialog } from '@/views/dialogs/TransferDialog';
import { WithdrawDialog } from '@/views/dialogs/WithdrawDialog';
import { OrderDetailsDialog } from '@/views/dialogs/DetailsDialog/OrderDetailsDialog';
import { FillDetailsDialog } from '@/views/dialogs/DetailsDialog/FillDetailsDialog';
@@ -53,5 +54,6 @@ export const DialogManager = () => {
[DialogTypes.Receive]: <ReceiveDialog {...modalProps} />,
[DialogTypes.Trade]: <TradeDialog {...modalProps} />,
[DialogTypes.Transfer]: <TransferDialog {...modalProps} />,
[DialogTypes.Withdraw]: <WithdrawDialog {...modalProps} />,
}[type];
};
+1 -1
View File
@@ -1,4 +1,4 @@
import { OrderSide } from '@dydxprotocol/v4-client';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import {
AbacusOrderSide,
+1 -1
View File
@@ -8,7 +8,7 @@ import {
Network,
NetworkOptimizer,
ValidatorConfig,
} from '@dydxprotocol/v4-client';
} from '@dydxprotocol/v4-client-js';
import {
type AbacusDydxChainQueriesProtocol,
+23
View File
@@ -0,0 +1,23 @@
import type { AbacusFileSystemProtocol, FileLocation, Nullable } from '@/constants/abacus';
import { NETWORK_ENDPOINTS } from '@/constants/networks';
export const ENDPOINTS_PATH =
import.meta.env.MODE === 'production'
? 'config/prod/endpoints.json'
: 'config/staging/dev_endpoints.json';
class AbacusFileSystem implements AbacusFileSystemProtocol {
readTextFile(location: FileLocation, path: string): Nullable<string> {
if (path === ENDPOINTS_PATH) {
return JSON.stringify(NETWORK_ENDPOINTS);
}
return null;
}
writeTextFile(path: string, text: string): boolean {
return true;
}
}
export default AbacusFileSystem;
+31 -2
View File
@@ -18,6 +18,7 @@ import {
IOImplementations,
UIImplementations,
CoroutineTimer,
TransferType,
} from '@/constants/abacus';
import { DEFAULT_MARKETID } from '@/constants/markets';
@@ -26,6 +27,7 @@ import { type DydxNetwork } from '@/constants/networks';
import type { RootStore } from '@/state/_store';
import { getInputTradeOptions } from '@/state/inputsSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
import AbacusRest from './rest';
import AbacusWebsocket from './websocket';
@@ -34,8 +36,8 @@ import AbacusStateNotifier from './stateNotification';
import AbacusLocalizer from './localizer';
import AbacusFormatter from './formatter';
import AbacusThreading from './threading';
import AbacusFileSystem, { ENDPOINTS_PATH } from './filesystem';
import { LocaleSeparators } from '../numbers';
class AbacusStateManager {
private store: RootStore | undefined;
private currentMarket: string | undefined;
@@ -60,7 +62,7 @@ class AbacusStateManager {
null,
new AbacusThreading(),
new CoroutineTimer(),
null
new AbacusFileSystem()
);
const uiImplementations = new UIImplementations(
@@ -70,6 +72,8 @@ class AbacusStateManager {
);
this.stateManager = new AsyncAbacusStateManager(
import.meta.env.SHARED_RESOURCES_URI,
ENDPOINTS_PATH,
ioImplementations,
uiImplementations,
// @ts-ignore
@@ -90,6 +94,15 @@ class AbacusStateManager {
disconnectAccount = () => {
this.stateManager.accountAddress = null;
};
attemptDisconnectAccount = () => {
const state = this.store?.getState();
const { type: transferType }= (state && getTransferInputs(state)) || {};
// we don't want to disconnect the account if we switch network during the deposit form
if (transferType?.rawValue !== TransferType.deposit.rawValue) {
this.disconnectAccount();
}
}
// ------ Input Values ------ //
clearTradeInputValues = ({ shouldResetSize }: { shouldResetSize?: boolean } = {}) => {
@@ -147,6 +160,10 @@ class AbacusStateManager {
this.stateManager.accountAddress = walletAddress;
};
setTransfersSourceAddress = (evmAddress: string) => {
this.stateManager.sourceAddress = evmAddress;
};
setSubaccountNumber = (subaccountNumber: number) =>
(this.stateManager.subaccountNumber = subaccountNumber);
@@ -186,6 +203,18 @@ class AbacusStateManager {
this.abacusFormatter.setLocaleSeparators({ group, decimal });
};
setTransferStatus = ({
hash,
fromChainId,
toChainId,
}: {
hash: string;
fromChainId?: string;
toChainId?: string;
}) => {
this.stateManager.transferStatus(hash, fromChainId, toChainId);
};
// ------ Utils ------ //
placeOrderPayload = (): Nullable<HumanReadablePlaceOrderPayload> =>
this.stateManager.placeOrderPayload();
+11
View File
@@ -1,5 +1,8 @@
import { kollections } from '@dydxprotocol/abacus';
import type {
AbacusApiState,
AbacusNotification,
AbacusStateNotificationProtocol,
Asset,
Nullable,
@@ -36,6 +39,14 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol {
this.store = undefined;
}
environmentsChanged(): void {
return;
}
notificationsChanged(notifications: kollections.List<AbacusNotification>): void {
return;
}
stateChanged(
updatedState: Nullable<PerpetualState>,
incomingChanges: Nullable<PerpetualStateChanges>
-140
View File
@@ -1,140 +0,0 @@
import { RouteResponse, Squid } from '@0xsquid/sdk';
import type { GetRoute, ChainData, RouteData, TokenData } from '@0xsquid/sdk';
import { DEFAULT_APP_ENVIRONMENT, DydxV4Network } from '@/constants/networks';
import { log } from './telemetry';
/**
* @todo dYdX chain will eventually be added to squidSDK as an option for toChain and
* USDC from Noble chain will also be a possible toToken in the near future. We will
* need to eventually change these constants.
* */
const SQUID_TESTNET_ROUTE_DEFAULTS = {
toChain: 'osmo-test-4', // Osmosis Testnet
toToken: 'uausdc', // aUSDC
enableForecall: true, // instant execution service, defaults to true
quoteOnly: false, // optional, defaults to false
};
// This config is for dev only! Used for testing General Message Passing (route provided by 0xSquid)
const SQUID_TESTNET_AVAX_TO_AXELAR_WITH_GMP = {
toChain: 'axelar-testnet-lisbon-3',
toToken: 'uosmo',
enableForecall: false,
quoteOnly: false,
};
const SQUID_MAINNET_ROUTE_DEFAULTS = {
toChain: 'osmosis-1', // Osmosis Testnet
toToken: 'uusdc', // aUSDC
enableForecall: true, // instant execution service, defaults to true
quoteOnly: false, // optional, defaults to false
};
class SquidFactory {
private client: Squid;
private isInitialized: boolean;
constructor(baseUrl: string) {
this.client = new Squid({ baseUrl });
this.isInitialized = false;
}
async init() {
if (this.isInitialized) return;
try {
await this.client.init();
this.isInitialized = true;
} catch (error) {
log('SquidFactory/init', error);
}
}
getClient = () => this.client;
}
const squidClientTestnet = new SquidFactory('https://testnet.api.0xsquid.com'); // new SquidFactory('https://squid-2gwsw7ij1-0xsquid.vercel.app');
const squidClientMainnet = new SquidFactory('https://api.0xsquid.com');
const SQUID_CLIENT_BY_NETWORK: Record<DydxV4Network, SquidFactory> = {
[DydxV4Network.V4Local]: squidClientTestnet,
[DydxV4Network.V4Mainnet]: squidClientMainnet,
[DydxV4Network.V4Staging]: squidClientTestnet,
[DydxV4Network.V4Testnet2]: squidClientTestnet,
};
class SquidRouter {
private client: Squid;
private network: DydxV4Network;
constructor() {
this.network = DEFAULT_APP_ENVIRONMENT;
this.client = SQUID_CLIENT_BY_NETWORK[this.network].getClient();
}
/**
* @description the default destination chain/token and other hardcoded options
*/
get SQUID_ROUTE_DEFAULTS() {
return {
[DydxV4Network.V4Local]: SQUID_TESTNET_ROUTE_DEFAULTS,
[DydxV4Network.V4Staging]: SQUID_TESTNET_ROUTE_DEFAULTS,
[DydxV4Network.V4Testnet2]: SQUID_TESTNET_ROUTE_DEFAULTS,
[DydxV4Network.V4Mainnet]: SQUID_MAINNET_ROUTE_DEFAULTS,
}[this.network];
}
/**
* @description updates the client to the correct 0xSquid environment based on which dYdX v4 network is selected.
* @param network dYdX v4 network that is currently selected
*/
switchNetwork = (network: DydxV4Network) => {
this.client = SQUID_CLIENT_BY_NETWORK[network].getClient();
};
/**
* @description initializes the Squid client if it was not previously initialized (API request)
*/
initializeClient = () => SQUID_CLIENT_BY_NETWORK[this.network].init();
/**
* @returns list of chains supported by Axelar
*/
getChains = (): ChainData[] => this.client.chains;
/**
* @returns list of tokens supported by Axelar
*/
getTokens = (): TokenData[] => this.client.tokens;
/**
* @param getRouteParams
* @returns 0xSquid RouteData
*/
getRoute = async (getRouteParams: GetRoute): Promise<RouteResponse> => {
const resp = await this.client.getRoute(getRouteParams);
return resp;
};
/**
* @params {
* @route the 0xSquid RouteData
* @signer the EVM/Cosmos Signer used to sign the Axelar bridge
* }
* @returns transaction information after the route has been executed
*/
executeRoute = async ({ route, signer }: { route: RouteData; signer: any }) => {
const tx = await this.client.executeRoute({
signer,
route,
});
return await tx.wait();
};
}
const squidRouter = new SquidRouter();
export default squidRouter;
+1 -1
View File
@@ -1,5 +1,5 @@
import { type Location, matchPath } from 'react-router-dom';
import { OrderSide } from '@dydxprotocol/v4-client';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { StringGetterFunction } from '@/constants/localization';
+15 -12
View File
@@ -162,7 +162,13 @@ const createWalletConnect2ConnectorWithId = (walletconnect2Id: string) =>
});
// Custom connector from wallet selection
import { type WalletConnection, WalletConnectionType, type WalletType, walletConnectionTypes, wallets } from '@/constants/wallets';
import {
type WalletConnection,
WalletConnectionType,
type WalletType,
walletConnectionTypes,
wallets,
} from '@/constants/wallets';
export const resolveWagmiConnector = ({
walletType,
@@ -174,15 +180,12 @@ export const resolveWagmiConnector = ({
const walletConfig = wallets[walletType];
const walletConnectionConfig = walletConnectionTypes[walletConnection.type];
return (
walletConnection.type === WalletConnectionType.InjectedEip1193 && walletConnection.provider ?
createInjectedConnectorWithProvider(walletConnection.provider)
: walletConnection.type === WalletConnectionType.WalletConnect1 && walletConfig.walletconnect1Name ?
createWalletConnect1ConnectorWithName(walletConfig.walletconnect1Name)
: walletConnection.type === WalletConnectionType.WalletConnect2 && walletConfig.walletconnect2Id ?
createWalletConnect2ConnectorWithId(walletConfig.walletconnect2Id)
: connectors.find(
({ id }: { id: string }) => id === walletConnectionConfig.wagmiConnectorId
)
);
return walletConnection.type === WalletConnectionType.InjectedEip1193 && walletConnection.provider
? createInjectedConnectorWithProvider(walletConnection.provider)
: walletConnection.type === WalletConnectionType.WalletConnect1 &&
walletConfig.walletconnect1Name
? createWalletConnect1ConnectorWithName(walletConfig.walletconnect1Name)
: walletConnection.type === WalletConnectionType.WalletConnect2 && walletConfig.walletconnect2Id
? createWalletConnect2ConnectorWithId(walletConfig.walletconnect2Id)
: connectors.find(({ id }: { id: string }) => id === walletConnectionConfig.wagmiConnectorId);
};
+18 -1
View File
@@ -479,6 +479,7 @@
"LEAVING_WEBSITE": "Leaving website"
},
"DEPOSIT_MODAL": {
"BRIDGE_FEE": "Bridge Fee",
"ENABLE_FOR_THIS_DEPOSIT": "Enable for this deposit",
"ENABLE_SYMBOL_ON_DYDX": "Enable {SYMBOL} on dYdX",
"ENABLE_SYMBOL": "Enable {SYMBOL}",
@@ -944,7 +945,8 @@
"SLOW_WITHDRAW_PENDING_DESCRIPTION": "You have {AMOUNT_ELEMENT} in pending slow withdraws. You will receive a notification when your funds have been confirmed on Layer 2.",
"SLOW_WITHDRAW_PENDING": "Slow withdraw(s) pending",
"SUSPICIOUS_TRADE": "Suspicious activity",
"SUSPICIOUS_TRADE_BODY": "We have noticed suspicious trading activity related to your account on dYdX, including potential wash trading activity. We will be reviewing your activity on an ongoing basis. If we notice similar activity in the future, then you will be permanently blocked from performing transfers within the protocol and from placing orders other than market orders that reduce your positions."
"SUSPICIOUS_TRADE_BODY": "We have noticed suspicious trading activity related to your account on dYdX, including potential wash trading activity. We will be reviewing your activity on an ongoing basis. If we notice similar activity in the future, then you will be permanently blocked from performing transfers within the protocol and from placing orders other than market orders that reduce your positions.",
"WITHDRAW_IN_PROGRESS": "Withdrawal(s) in progress"
},
"EMAIL_NOTIFICATIONS": {
"ACCOUNT_DESCRIPTION": "Deposits, Withdrawals, Account Updates",
@@ -1207,5 +1209,20 @@
"NODE_DOWN": "Node cannot be reached.",
"NODE_HALTED": "Node has halted on block {BLOCK_NUMBER}.",
"UNKNOWN_API_ERROR": "Unknown API error"
},
"CHAIN": {
"V3_MAINNET": "V3 Mainnet",
"V3_GOERLI": "V3 Staging",
"V4_DEVNET": "V4 Dev 1",
"V4_DEVNET_2": "V4 Dev 2",
"V4_DEVNET_3": "V4 Dev 3",
"V4_DEVNET_4": "V4 Dev 4",
"V4_DEVNET_5": "V4 Dev 5",
"V4_STAGING": "V4 Staging",
"V4_TESTNET2": "V4 Testnet",
"V4_TESTNET2_DSRV": "V4 Testnet (DSRV)",
"V4_TESTNET2_DYDX": "V4 Testnet (dYdX)",
"V4_TESTNET2_NODEFLEET": "V4 Testnet (nodefleet)",
"V4_TESTNET2_IMPERATOR": "V4 Testnet (Imperator)"
}
}
+4 -1
View File
@@ -65,7 +65,10 @@
"MIN_SLOW_WITHDRAW_AMOUNT": "Slow withdraw amount must be greater than or equal to 10, or your entire account balance.",
"RESTRICTED_OPEN_POSITIONS_WITHDRAW": "Your account is restricted. You must close all your positions before you can withdraw.",
"WITHDRAW_MORE_THAN_FREE_DUE_TO_FEE": "You cannot withdraw more than your free collateral. Make sure you account for the fast withdraw fee when entering an amount.",
"WITHDRAW_MORE_THAN_FREE": "You cannot withdraw more than your free collateral."
"WITHDRAW_MORE_THAN_FREE": "You cannot withdraw more than your free collateral.",
"WITHDRAW_MUST_SPECIFY_ADDRESS": "Please enter a destination address.",
"WITHDRAW_MUST_SPECIFY_ASSET": "Please select an asset to withdraw to.",
"WITHDRAW_MUST_SPECIFY_CHAIN": "Please select a source chain to withdraw to."
},
"TRANSFER_MODAL": {
"TRANSFER_INVALID_ETH_ADDRESS": "We could not find an account associated with this wallet address.",
+1 -1
View File
@@ -1,7 +1,7 @@
import { createSelector } from 'reselect';
import { SubaccountPosition } from '@/constants/abacus';
import { OnboardingGuard, OnboardingState, OnboardingSteps } from '@/constants/account';
import { OnboardingState, OnboardingSteps } from '@/constants/account';
import { isDydxV4Network } from '@/constants/networks';
import {
+1 -1
View File
@@ -1,4 +1,4 @@
import { OrderSide } from '@dydxprotocol/v4-client';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { createSelector } from 'reselect';
import {
-6
View File
@@ -2,13 +2,11 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { AbacusApiStatus } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs';
import { isDydxV4Network } from '@/constants/networks';
import { setApiState, setSelectedNetwork } from '@/state/app';
import { resetPerpetualsState } from '@/state/perpetuals';
import abacusStateManager from '@/lib/abacus';
import squidRouter from '@/lib/squidRouter';
import { openDialog } from './dialogs';
import { getActiveDialog } from './dialogsSelectors';
@@ -22,10 +20,6 @@ export default (store: any) => (next: any) => async (action: PayloadAction<any>)
case setSelectedNetwork.type: {
store.dispatch(resetPerpetualsState());
abacusStateManager.switchNetwork(payload);
if (isDydxV4Network(payload)) {
squidRouter.switchNetwork(payload);
}
break;
}
case setApiState.type: {
+1 -1
View File
@@ -61,7 +61,7 @@ export const popoverMixins = {
${layoutMixins.spacedRow}
grid-template-columns: 1fr auto;
padding-right: 1rem;
gap: 0;
border: 1px solid var(--color-layer-6);
`,
@@ -72,16 +72,26 @@ export const AccountInfoConnectedState = () => {
{!showHeader ? null : (
<Styled.Header>
<span>{stringGetter({ key: STRING_KEYS.ACCOUNT })}</span>
<Styled.Button
state={{ isDisabled: !dydxAccounts }}
onClick={() => dispatch(openDialog({ type: DialogTypes.Deposit }))}
shape={ButtonShape.Pill}
size={ButtonSize.XSmall}
slotLeft={<Icon iconName={IconName.Transfer} />}
>
{stringGetter({ key: STRING_KEYS.MANAGE_FUNDS })}
</Styled.Button>
<Styled.TransferButtons>
{import.meta.env.MODE !== 'production' && (
<Styled.Button
state={{ isDisabled: !dydxAccounts }}
onClick={() => dispatch(openDialog({ type: DialogTypes.Withdraw }))}
shape={ButtonShape.Pill}
size={ButtonSize.XSmall}
>
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
</Styled.Button>
)}
<Styled.Button
state={{ isDisabled: !dydxAccounts }}
onClick={() => dispatch(openDialog({ type: DialogTypes.Deposit }))}
shape={ButtonShape.Pill}
size={ButtonSize.XSmall}
>
{stringGetter({ key: STRING_KEYS.DEPOSIT })}
</Styled.Button>
</Styled.TransferButtons>
</Styled.Header>
)}
<Styled.Stack>
@@ -273,6 +283,11 @@ Styled.Header = styled.header`
padding: 0 1.25rem;
`;
Styled.TransferButtons = styled.div`
${layoutMixins.inlineRow}
gap: 1rem;
`;
Styled.ConnectedAccountInfoContainer = styled.div<{ $showHeader?: boolean }>`
${layoutMixins.column}
${layoutMixins.withOuterAndInnerBorders}
+1 -1
View File
@@ -33,7 +33,7 @@ import { Details } from '@/components/Details';
import { LoadingSpace } from '@/components/Loading/LoadingSpinner';
import { Output, OutputType } from '@/components/Output';
import { OrderSide } from '@dydxprotocol/v4-client';
import { OrderSide } from '@dydxprotocol/v4-client-js';
// @ts-ignore
const theme = buildChartTheme({
+4 -4
View File
@@ -6,8 +6,8 @@ import { useBreakpoints, useStringGetter } from '@/hooks';
import { Dialog, DialogPlacement } from '@/components/Dialog';
import { DepositForm } from '@/views/forms/DepositForm';
import { TestnetDepositForm } from '@/views/forms/TestnetDepositForm';
import { DepositForm } from '@/views/forms/AccountManagementForms/DepositForm';
import { TestnetDepositForm } from '@/views/forms/AccountManagementForms/TestnetDepositForm';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -32,12 +32,12 @@ export const DepositDialog = ({ setIsOpen }: ElementProps) => {
>
<Styled.Content>
{showTestDeposit ? <TestnetDepositForm onDeposit={closeDialog} /> : <DepositForm />}
{/* TODO TRCL-1693 - uncomment when deposit form is ready
{/* TODO TRCL-1693 - uncomment when deposit form is ready */}
{import.meta.env.MODE !== 'production' && (
<Styled.TextToggle onClick={() => setShowTestDeposit(!showTestDeposit)}>
{showTestDeposit ? 'Show deposit form (Under Construction)' : 'Show test faucet'}
</Styled.TextToggle>
)} */}
)}
</Styled.Content>
</Dialog>
);
+2 -2
View File
@@ -3,7 +3,7 @@ import styled, { AnyStyledComponent } from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import { AbacusApiStatus } from '@/constants/abacus';
import { DydxV4Network } from '@/constants/networks';
import { CLIENT_NETWORK_CONFIGS } from '@/constants/networks';
import { UNICODE } from '@/constants/unicode';
import { STRING_KEYS } from '@/constants/localization';
@@ -44,7 +44,7 @@ export const ExchangeOfflineDialog = ({ preventClose, setIsOpen }: ElementProps)
>
<Styled.Content>
<p>{statusErrorMessage}</p>
{selectedNetwork === DydxV4Network.V4Testnet2 && (
{CLIENT_NETWORK_CONFIGS[selectedNetwork].dydxChainId === 'dydx-testnet-2' && (
<Styled.Link href="https://status.v4testnet.dydx.exchange">
{stringGetter({ key: STRING_KEYS.LEARN_MORE })} {UNICODE.ARROW_RIGHT}
</Styled.Link>
+1 -1
View File
@@ -19,7 +19,7 @@ import { GreenCheckCircle } from '@/components/GreenCheckCircle';
import { Icon } from '@/components/Icon';
import { Ring } from '@/components/Ring';
import { TestnetDepositForm } from '@/views/forms/TestnetDepositForm';
import { TestnetDepositForm } from '@/views/forms/AccountManagementForms/TestnetDepositForm';
import { track } from '@/lib/analytics';
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { useSignTypedData } from 'wagmi';
import styled, { type AnyStyledComponent, css } from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { AES } from 'crypto-js';
import { EvmDerivedAccountStatus } from '@/constants/account';
@@ -9,7 +9,7 @@ import { AlertType } from '@/constants/alerts';
import { AnalyticsEvent } from '@/constants/analytics';
import { ButtonAction } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { CLIENT_NETWORK_CONFIGS, DydxV4Network } from '@/constants/networks';
import { CLIENT_NETWORK_CONFIGS } from '@/constants/networks';
import { DydxAddress, SIGN_TYPED_DATA } from '@/constants/wallets';
import { useAccounts, useBreakpoints, useDydxClient, useStringGetter } from '@/hooks';
@@ -54,7 +54,7 @@ export const GenerateKeys = ({
// 1. Switch network
const selectedNetwork = useSelector(getSelectedNetwork);
const chainId = Number(CLIENT_NETWORK_CONFIGS[selectedNetwork as DydxV4Network].ethereumChainId);
const chainId = Number(CLIENT_NETWORK_CONFIGS[selectedNetwork].ethereumChainId);
const { isMatchingNetwork, matchNetwork, isSwitchingNetwork } = useMatchingEvmNetwork({
chainId,
+56
View File
@@ -0,0 +1,56 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { Dialog, DialogPlacement } from '@/components/Dialog';
import { WithdrawForm } from '@/views/forms/AccountManagementForms/WithdrawForm';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
setIsOpen: (open: boolean) => void;
};
export const WithdrawDialog = ({ setIsOpen }: ElementProps) => {
const stringGetter = useStringGetter();
const { isTablet } = useBreakpoints();
return (
<Dialog
isOpen
setIsOpen={setIsOpen}
title={stringGetter({ key: STRING_KEYS.WITHDRAW })}
placement={isTablet ? DialogPlacement.FullScreen : DialogPlacement.Default}
>
<Styled.Content>
<WithdrawForm />
</Styled.Content>
</Dialog>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.TextToggle = styled.div`
${layoutMixins.stickyFooter}
color: var(--color-accent);
cursor: pointer;
margin-top: auto;
&:hover {
text-decoration: underline;
}
`;
Styled.Content = styled.div`
${layoutMixins.stickyArea0}
--stickyArea0-bottomHeight: 2rem;
--stickyArea0-bottomGap: 1rem;
--stickyArea0-totalInsetBottom: 0.5rem;
${layoutMixins.flexColumn}
gap: 1rem;
`;
@@ -1,31 +1,45 @@
import type { ChainData } from '@0xsquid/sdk';
import styled, { type AnyStyledComponent } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import { TransferType } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
import { useStringGetter } from '@/hooks';
import { WithLabel } from '@/components/WithLabel';
import { SearchSelectMenu } from '@/components/SearchSelectMenu';
import { layoutMixins } from '@/styles/layoutMixins';
import { popoverMixins } from '@/styles/popoverMixins';
import { getTransferInputs } from '@/state/inputsSelectors';
type ElementProps = {
chains?: ChainData[];
currentChain?: ChainData;
setCurrentChain: (chain: ChainData) => void;
label?: string;
selectedChain?: string;
onSelectChain: (chain: string) => void;
};
export const ChainSelectMenu = ({ chains = [], currentChain, setCurrentChain }: ElementProps) => {
export const ChainSelectMenu = ({
label,
selectedChain,
onSelectChain,
}: ElementProps) => {
const stringGetter = useStringGetter();
const { type, depositOptions, withdrawalOptions, resources } =
useSelector(getTransferInputs, shallowEqual) || {};
const chains =
(type === TransferType.deposit ? depositOptions : withdrawalOptions)?.chains?.toArray() || [];
const chainItems = Object.values(chains).map((chain: ChainData) => ({
value: chain.chainId.toString(),
label: chain.chainName,
onSelect: () => setCurrentChain(chain),
slotBefore: <Styled.Img src={chain.chainIconURI} alt="" />,
const chainItems = Object.values(chains).map((chain) => ({
value: chain.type,
label: chain.stringKey,
onSelect: () => {
onSelectChain(chain.type);
},
slotBefore: <Styled.Img src={chain.iconUrl} alt="" />,
}));
const selectedOption = chains.find((item) => item.type === selectedChain);
return (
<SearchSelectMenu
items={[
@@ -35,12 +49,12 @@ export const ChainSelectMenu = ({ chains = [], currentChain, setCurrentChain }:
items: chainItems,
},
]}
label="Source"
label={label || (type === TransferType.deposit ? 'Source' : 'Destination')}
>
<Styled.ChainRow>
{currentChain ? (
{selectedChain ? (
<>
<Styled.Img src={currentChain?.chainIconURI} alt="" /> {currentChain?.chainName}
<Styled.Img src={selectedOption?.iconUrl} alt="" /> {selectedOption?.stringKey}
</>
) : (
stringGetter({ key: STRING_KEYS.SELECT_CHAIN })
@@ -67,4 +81,4 @@ Styled.ChainRow = styled.div`
gap: 0.5rem;
color: var(--color-text-2);
font: var(--font-base-book);
`;
`;
@@ -0,0 +1,342 @@
import { type FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { type NumberFormatValues } from 'react-number-format';
import { shallowEqual, useSelector } from 'react-redux';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
import { ethers } from 'ethers';
import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus';
import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { NumberSign } from '@/constants/numbers';
import { useAccounts, useDebounce, useStringGetter } from '@/hooks';
import { useAccountBalance } from '@/hooks/useAccountBalance';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
import { AlertMessage } from '@/components/AlertMessage';
import { Button } from '@/components/Button';
import { DiffOutput } from '@/components/DiffOutput';
import { FormInput } from '@/components/FormInput';
import { InputType } from '@/components/Input';
import { Link } from '@/components/Link';
import { LoadingSpace } from '@/components/Loading/LoadingSpinner';
import { OutputType } from '@/components/Output';
import { Tag } from '@/components/Tag';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { getTransferInputs } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers';
import { ChainSelectMenu } from './ChainSelectMenu';
import { TokenSelectMenu } from './TokenSelectMenu';
import { DepositButtonAndReceipt } from './DepositForm/DepositButtonAndReceipt';
type DepositFormProps = {
onDeposit?: () => void;
onError?: () => void;
};
export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const stringGetter = useStringGetter();
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(false);
const { signerWagmi } = useAccounts();
const {
requestPayload,
token,
chain: chainIdStr,
resources,
} = useSelector(getTransferInputs, shallowEqual) || {};
const chainId = chainIdStr ? parseInt(chainIdStr) : undefined;
// User inputs
const sourceToken = useMemo(
() => (token ? resources?.tokenResources?.get(token) : undefined),
[token, resources]
);
const [fromAmount, setFromAmount] = useState('');
const [slippage, setSlippage] = useState(0.01); // 1% slippage
const debouncedAmount = useDebounce<string>(fromAmount, 500);
// Async Data
const [transactionHash, setTransactionHash] = useState<string>();
const { balance, queryStatus, isQueryFetching } = useAccountBalance({
addressOrDenom: sourceToken?.address || undefined,
assetSymbol: sourceToken?.symbol || undefined,
chainId: chainId,
decimals: sourceToken?.decimals || undefined,
isCosmosChain: false,
});
// BN
const debouncedAmountBN = MustBigNumber(debouncedAmount);
const balanceBN = MustBigNumber(balance);
useEffect(() => {
const hasInvalidInput =
debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0) || debouncedAmountBN.gte(balanceBN);
abacusStateManager.setTransferValue({
value: hasInvalidInput ? 0 : debouncedAmount,
field: TransferInputField.size,
});
setError(null);
}, [debouncedAmountBN.toNumber()]);
useEffect(() => {
abacusStateManager.setTransferValue({
field: TransferInputField.type,
value: TransferType.deposit.rawValue,
});
return () => {
abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.type,
value: null,
});
};
}, []);
useEffect(() => {
if (error) onError?.();
}, [error]);
const onSelectChain = useCallback((chain: string) => {
if (chain) {
abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.chain,
value: chain,
});
setFromAmount('');
}
}, []);
const onSelectToken = useCallback((token: TransferInputTokenResource) => {
if (token) {
abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.token,
value: token.address,
});
setFromAmount('');
}
}, []);
const onChangeAmount = useCallback(
({ value }: NumberFormatValues) => {
setFromAmount(value);
},
[setFromAmount]
);
const onSetSlippage = useCallback(
(newSlippage: number) => {
setSlippage(newSlippage);
// TODO: to be implemented via abacus
},
[setSlippage, debouncedAmount]
);
const onClickMax = useCallback(() => {
if (balance) {
setFromAmount(balanceBN.toString());
}
}, [balance, setFromAmount]);
const onSubmit = useCallback(
async (e: FormEvent) => {
try {
e.preventDefault();
if (!signerWagmi) {
throw new Error('Missing signer');
}
if (
!requestPayload?.targetAddress ||
!requestPayload.data ||
!requestPayload.value ||
!requestPayload.gasLimit ||
!requestPayload.gasPrice ||
!requestPayload.routeType
) {
throw new Error('Missing request payload');
}
setIsLoading(true);
let tx = {
to: requestPayload.targetAddress as `0x${string}`,
data: requestPayload.data as `0x${string}`,
gasLimit: ethers.toBigInt(requestPayload.gasLimit),
value:
requestPayload.routeType !== 'SEND' ? ethers.toBigInt(requestPayload.value) : undefined,
};
const txHash = await signerWagmi.sendTransaction(tx);
onDeposit?.();
if (txHash) {
setTransactionHash(txHash);
abacusStateManager.setTransferStatus({
hash: txHash,
toChainId: TESTNET_CHAIN_ID,
fromChainId: chainId?.toString(),
});
abacusStateManager.clearTransferInputValues();
setFromAmount('');
}
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
},
[requestPayload, signerWagmi, chainId]
);
const amountInputReceipt = [
{
key: 'amount',
label: (
<span>
{stringGetter({ key: STRING_KEYS.AVAILABLE })}{' '}
{sourceToken && <Tag>{sourceToken.symbol}</Tag>}
</span>
),
value: (
<DiffOutput
type={OutputType.Asset}
value={balance}
newValue={balanceBN.minus(debouncedAmountBN).toString()}
sign={NumberSign.Negative}
hasInvalidNewValue={balanceBN.minus(debouncedAmountBN).isNegative()}
withDiff={
Boolean(fromAmount && balance) &&
!debouncedAmountBN.isNaN() &&
!debouncedAmountBN.isZero()
}
/>
),
},
];
const errorMessage = useMemo(() => {
if (error) {
return error?.message
? stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: { ERROR_MESSAGE: error.message },
})
: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG });
}
if (fromAmount) {
if (!chainId) {
return stringGetter({ key: STRING_KEYS.MUST_SPECIFY_CHAIN });
} else if (!sourceToken) {
return stringGetter({ key: STRING_KEYS.MUST_SPECIFY_ASSET });
}
}
if (MustBigNumber(fromAmount).gt(MustBigNumber(balance))) {
return stringGetter({ key: STRING_KEYS.DEPOSIT_MORE_THAN_BALANCE });
}
return undefined;
}, [error, balance, chainId, fromAmount, sourceToken]);
const isDisabled =
Boolean(errorMessage) ||
!sourceToken ||
!chainId ||
debouncedAmountBN.isNaN() ||
debouncedAmountBN.isZero();
if (!resources) {
return <LoadingSpace id="DepositForm" />;
}
return (
<Styled.Form onSubmit={onSubmit}>
<ChainSelectMenu selectedChain={chainIdStr || undefined} onSelectChain={onSelectChain} />
<TokenSelectMenu selectedToken={sourceToken || undefined} onSelectToken={onSelectToken} />
<Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}>
<FormInput
type={InputType.Number}
onChange={onChangeAmount}
label={stringGetter({ key: STRING_KEYS.AMOUNT })}
value={fromAmount}
slotRight={
<Styled.FormInputButton size={ButtonSize.XSmall} onClick={onClickMax}>
{stringGetter({ key: STRING_KEYS.MAX })}
</Styled.FormInputButton>
}
/>
</Styled.WithDetailsReceipt>
{errorMessage ? (
<AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>
) : (
transactionHash && (
<AlertMessage type={AlertType.Success}>
<Styled.TransactionInfo>
{stringGetter({ key: STRING_KEYS.DEPOSIT_IN_PROGRESS })}
</Styled.TransactionInfo>
</AlertMessage>
)
)}
<DepositButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
chainId={chainId || undefined}
setSlippage={onSetSlippage}
slippage={slippage}
sourceToken={sourceToken || undefined}
/>
</Styled.Form>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Form = styled.form`
--form-input-height: 3.5rem;
${layoutMixins.flexColumn}
gap: 1.5rem;
${layoutMixins.stickyArea1}
min-height: calc(100% - var(--stickyArea0-bottomHeight));
`;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
`;
Styled.Link = styled(Link)`
color: var(--color-accent);
&:visited {
color: var(--color-accent);
}
`;
Styled.TransactionInfo = styled.span`
${layoutMixins.row}
`;
Styled.FormInputButton = styled(Button)`
${formMixins.inputInnerButton}
`;
@@ -1,8 +1,7 @@
import { type Dispatch, type SetStateAction, useState } from 'react';
import styled, { type AnyStyledComponent, css } from 'styled-components';
import { type Dispatch, type SetStateAction, useState, type ReactNode } from 'react';
import styled, { type AnyStyledComponent, } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import type { RouteData, TokenData } from '@0xsquid/sdk';
import BigNumber from 'bignumber.js';
import type { RouteData } from '@0xsquid/sdk';
import {
ButtonAction,
@@ -12,6 +11,7 @@ import {
ButtonType,
} from '@/constants/buttons';
import { TransferInputTokenResource } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
import { NumberSign } from '@/constants/numbers';
@@ -30,11 +30,9 @@ import { ToggleButton } from '@/components/ToggleButton';
import { WithReceipt } from '@/components/WithReceipt';
import { getSubaccountBuyingPower, getSubaccountEquity } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
import { BIG_NUMBERS, MustBigNumber } from '@/lib/numbers';
import squidRouter from '@/lib/squidRouter';
import { SlippageEditor } from './SlippageEditor';
import { SlippageEditor } from '../SlippageEditor';
type ElementProps = {
isDisabled?: boolean;
@@ -43,8 +41,9 @@ type ElementProps = {
chainId?: string | number;
setError?: Dispatch<SetStateAction<Error | undefined>>;
slippage: number;
slotError?: ReactNode;
setSlippage: (slippage: number) => void;
sourceToken?: TokenData;
sourceToken?: TransferInputTokenResource;
squidRoute?: RouteData;
};
@@ -54,10 +53,10 @@ export const DepositButtonAndReceipt = ({
slippage,
setSlippage,
sourceToken,
squidRoute,
isDisabled,
isLoading,
slotError,
}: ElementProps) => {
const [showFeeBreakdown, setShowFeeBreakdown] = useState(false);
const [isEditingSlippage, setIsEditingSlipapge] = useState(false);
@@ -78,35 +77,23 @@ export const DepositButtonAndReceipt = ({
const { current: buyingPower, postOrder: newBuyingPower } =
useSelector(getSubaccountBuyingPower, shallowEqual) || {};
const { gasCosts, feeCosts } = squidRoute?.estimate || {};
const { summary, requestPayload } = useSelector(getTransferInputs, shallowEqual) || {};
const totalGasCosts =
gasCosts?.reduce(
(acc: BigNumber, { amountUSD }: { amountUSD: string }) => acc.plus(MustBigNumber(amountUSD)),
BIG_NUMBERS.ZERO
) || BIG_NUMBERS.ZERO;
const feeSubitems: DetailsItem[] = [];
const totalFeeCosts =
feeCosts?.reduce(
(acc: BigNumber, { amountUSD }: { amountUSD: string }) => acc.plus(MustBigNumber(amountUSD)),
BIG_NUMBERS.ZERO
) || BIG_NUMBERS.ZERO;
const totalFees = totalGasCosts.plus(totalFeeCosts);
const feeSubitems: DetailsItem[] = feeCosts
? feeCosts.map(({ amountUSD, name }: { amountUSD: string; name: string }) => ({
key: name,
label: <span>{name}</span>,
value: <Output type={OutputType.Fiat} value={amountUSD} />,
}))
: [];
if (gasCosts) {
if (typeof summary?.gasFee === 'number') {
feeSubitems.push({
key: 'gas-fees',
label: <span>{stringGetter({ key: STRING_KEYS.GAS_FEE })}</span>,
value: <Output type={OutputType.Fiat} value={totalGasCosts} />,
value: <Output type={OutputType.Fiat} value={summary?.gasFee} />,
});
}
if (typeof summary?.bridgeFee === 'number') {
feeSubitems.push({
key: 'bridge-fees',
label: <span>Bridge Fee</span>,
value: <Output type={OutputType.Fiat} value={summary?.bridgeFee} />,
});
}
@@ -128,9 +115,9 @@ export const DepositButtonAndReceipt = ({
<DiffOutput
type={OutputType.Fiat}
value={equity}
newValue={newEquity}
newValue={newEquity} // using toAmountUSD as a proxy for equity until Abacus supports accounts with no funds.
sign={NumberSign.Positive}
withDiff={Boolean(newEquity) && equity !== newEquity}
withDiff={equity !== newEquity}
/>
),
},
@@ -154,27 +141,20 @@ export const DepositButtonAndReceipt = ({
{
key: 'exchange-rate',
label: <span>{stringGetter({ key: STRING_KEYS.EXCHANGE_RATE })}</span>,
value: sourceToken && squidRoute && (
value: typeof summary?.exchangeRate === 'number' && (
<Styled.ExchangeRate>
<Output
type={OutputType.Asset}
value={1}
fractionDigits={0}
tag={<Tag>{sourceToken?.symbol}</Tag>}
/>
<Output type={OutputType.Asset} value={1} fractionDigits={0} tag={sourceToken?.symbol} />
=
<Output
type={OutputType.Asset}
value={squidRoute?.estimate?.exchangeRate}
tag={<Tag>{squidRouter.SQUID_ROUTE_DEFAULTS.toToken.toUpperCase()}</Tag>}
/>
<Output type={OutputType.Asset} value={summary?.exchangeRate} tag="USDC" />
</Styled.ExchangeRate>
),
},
{
key: 'total-fees',
label: <span>{stringGetter({ key: STRING_KEYS.TOTAL_FEES })}</span>,
value: squidRoute && <Output type={OutputType.Fiat} value={totalFees} />,
value: typeof summary?.bridgeFee === 'number' && typeof summary?.gasFee === 'number' && (
<Output type={OutputType.Fiat} value={summary?.bridgeFee + summary?.gasFee} />
),
subitems: feeSubitems,
},
{
@@ -212,17 +192,25 @@ export const DepositButtonAndReceipt = ({
</Styled.DetailButtons>
</Styled.CollapsibleDetails>
}
slotError={slotError}
>
{!isMatchingNetwork ? (
<Button
action={ButtonAction.Primary}
onClick={switchNetwork}
state={{ isLoading: isSwitchingNetwork || isLoading }}
state={{ isLoading: isSwitchingNetwork }}
>
{stringGetter({ key: STRING_KEYS.SWITCH_NETWORK })}
</Button>
) : (
<Button type={ButtonType.Submit} state={{ isDisabled: !isFormValid, isLoading }}>
<Button
action={ButtonAction.Primary}
type={ButtonType.Submit}
state={{
isDisabled: !isFormValid,
isLoading: (isFormValid && !requestPayload) || isLoading,
}}
>
{stringGetter({ key: STRING_KEYS.DEPOSIT_FUNDS })}
</Button>
)}
@@ -234,7 +222,7 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.ExchangeRate = styled.span`
${layoutMixins.row}
gap: 1ch;
gap: 0.5ch;
`;
Styled.WithReceipt = styled(WithReceipt)`
@@ -12,6 +12,7 @@ import { Icon, IconName } from '@/components/Icon';
import { Input, InputType } from '@/components/Input';
import { Output, OutputType } from '@/components/Output';
import { ToggleGroup } from '@/components/ToggleGroup';
import { FormInput } from '@/components/FormInput';
enum EditorState {
Viewing = 'Viewing',
@@ -115,7 +116,7 @@ export const SlippageEditor = ({ slippage, setIsEditing, setSlippage }: Slippage
</Styled.SlippageInput>
),
[EditorState.Editing]: (
<Styled.Input
<Styled.FormInput
type={InputType.Percent}
value={slippageInputValue}
tabIndex={0}
@@ -151,10 +152,8 @@ Styled.SlippageInput = styled.div`
border-radius: 0.5em;
`;
Styled.Input = styled(Input)`
// TODO(@aforaleka): replace with input container
--input-height: 1.5rem;
--input-backgroundColor: var(--color-layer-5);
Styled.FormInput = styled(FormInput)`
--form-input-height: 1.5rem;
input {
text-align: end;
@@ -0,0 +1,95 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import { STRING_KEYS } from '@/constants/localization';
import { TransferInputTokenResource, TransferType } from '@/constants/abacus';
import { useStringGetter } from '@/hooks';
import { DiffArrow } from '@/components/DiffArrow';
import { Icon, IconName } from '@/components/Icon';
import { SearchSelectMenu } from '@/components/SearchSelectMenu';
import { Tag } from '@/components/Tag';
import { layoutMixins } from '@/styles/layoutMixins';
import { getTransferInputs } from '@/state/inputsSelectors';
type ElementProps = {
selectedToken?: TransferInputTokenResource;
onSelectToken: (token: TransferInputTokenResource) => void;
};
export const TokenSelectMenu = ({ selectedToken, onSelectToken }: ElementProps) => {
const stringGetter = useStringGetter();
const { type, depositOptions, withdrawalOptions, resources } =
useSelector(getTransferInputs, shallowEqual) || {};
const tokens =
(type === TransferType.deposit ? depositOptions : withdrawalOptions)?.assets?.toArray() || [];
const tokenItems = Object.values(tokens).map((token) => ({
value: token.type,
label: token.stringKey,
onSelect: () => {
const selectedToken = resources?.tokenResources?.get(token.type);
selectedToken && onSelectToken(selectedToken);
},
slotBefore: <Styled.Img src={token.iconUrl} alt="" />,
}));
return (
<SearchSelectMenu
items={[
{
group: 'assets',
groupLabel: stringGetter({ key: STRING_KEYS.ASSET }),
items: tokenItems,
},
]}
label={stringGetter({ key: STRING_KEYS.ASSET })}
withReceiptItems={[
{
key: 'swap',
label: stringGetter({ key: STRING_KEYS.SWAP }),
value: selectedToken && (
<>
<Tag>{type === TransferType.deposit ? selectedToken?.symbol : 'USDC'}</Tag>
<DiffArrow />
<Tag>{type === TransferType.deposit ? 'USDC' : selectedToken?.symbol}</Tag>
</>
),
tooltip: 'swap',
},
]}
>
<Styled.AssetRow>
{selectedToken ? (
<>
<Styled.Img src={selectedToken?.iconUrl} alt="" /> {selectedToken?.name}{' '}
<Tag>{selectedToken?.symbol}</Tag>
</>
) : (
stringGetter({ key: STRING_KEYS.SELECT_ASSET })
)}
</Styled.AssetRow>
</SearchSelectMenu>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.AssetRow = styled.div`
${layoutMixins.row}
gap: 0.5rem;
color: var(--color-text-2);
font: var(--font-base-book);
`;
Styled.Img = styled.img`
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
`;
Styled.Icon = styled(Icon)`
height: 0.5rem;
`;
@@ -0,0 +1,358 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { ChangeEvent, FormEvent } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import type { NumberFormatValues } from 'react-number-format';
import { shallowEqual, useSelector } from 'react-redux';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus';
import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { NumberSign, QUANTUM_MULTIPLIER } from '@/constants/numbers';
import { useDebounce, useStringGetter, useSubaccount } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
import { AlertMessage } from '@/components/AlertMessage';
import { Button } from '@/components/Button';
import { DiffOutput } from '@/components/DiffOutput';
import { FormInput } from '@/components/FormInput';
import { InputType } from '@/components/Input';
import { Link } from '@/components/Link';
import { OutputType } from '@/components/Output';
import { Tag } from '@/components/Tag';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { ChainSelectMenu } from '@/views/forms/AccountManagementForms/ChainSelectMenu';
import { getSubaccount } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers';
import { TokenSelectMenu } from './TokenSelectMenu';
import { WithdrawButtonAndReceipt } from './WithdrawForm/WithdrawButtonAndReceipt';
export const WithdrawForm = () => {
const stringGetter = useStringGetter();
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(false);
const { simulateWithdraw, 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,
chain: chainIdStr,
address: toAddress,
resources,
} = useSelector(getTransferInputs, shallowEqual) || {};
const toToken = useMemo(
() => (token ? resources?.tokenResources?.get(token) : undefined),
[token, resources]
);
// Async Data
const [transactionHash, setTransactionHash] = useState<string>();
const debouncedAmountBN = MustBigNumber(debouncedAmount);
const withdrawAmountBN = MustBigNumber(withdrawAmount);
const freeCollateralBN = MustBigNumber(freeCollateral?.current);
useEffect(() => {
abacusStateManager.setTransferValue({
field: TransferInputField.type,
value: TransferType.withdrawal.rawValue,
});
return () => {
abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.type,
value: null,
});
};
}, []);
useEffect(() => {
const setTransferValue = async () => {
try {
setIsLoading(true);
const hasInvalidInput = debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0);
if (hasInvalidInput) {
abacusStateManager.setTransferValue({
value: 0,
field: TransferInputField.usdcSize,
});
} else {
const stdFee = await simulateWithdraw(parseFloat(debouncedAmount));
const amount =
parseFloat(debouncedAmount) -
parseFloat(stdFee?.amount[0]?.amount || '0') / QUANTUM_MULTIPLIER;
abacusStateManager.setTransferValue({
value: amount.toString(),
field: TransferInputField.usdcSize,
});
setError(null);
}
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
setTransferValue();
}, [debouncedAmountBN.toNumber()]);
const onSubmit = useCallback(
async (e: FormEvent) => {
try {
e.preventDefault();
if (!requestPayload?.data || !debouncedAmountBN.toNumber()) {
throw new Error('Invalid request payload');
}
setIsLoading(true);
const txHash = await sendSquidWithdraw(debouncedAmountBN.toNumber(), requestPayload?.data);
if (txHash?.hash) {
const hash = `0x${Buffer.from(txHash.hash).toString('hex')}`;
setTransactionHash(hash);
abacusStateManager.setTransferStatus({
hash,
fromChainId: TESTNET_CHAIN_ID,
toChainId: chainIdStr || undefined,
});
abacusStateManager.clearTransferInputValues();
setWithdrawAmount('');
}
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
},
[setTransactionHash, requestPayload, debouncedAmountBN, chainIdStr]
);
const onChangeAddress = useCallback((e: ChangeEvent<HTMLInputElement>) => {
abacusStateManager.setTransferValue({
field: TransferInputField.address,
value: e.target.value,
});
}, []);
const onChangeAmount = useCallback(
({ value }: NumberFormatValues) => {
setWithdrawAmount(value);
},
[setWithdrawAmount]
);
const onSetSlippage = useCallback(
(newSlippage: number) => {
setSlippage(newSlippage);
// TODO: to be implemented via abacus
// if (MustBigNumber(newSlippage).gt(0) && debouncedAmountBN.gt(0)) {
// fetchRoute({ newAmount: debouncedAmount, newSlippage });
// }
},
[setSlippage, debouncedAmount]
);
const onClickMax = useCallback(() => {
setWithdrawAmount(freeCollateralBN.toString());
}, [freeCollateral?.current, setWithdrawAmount]);
const onSelectChain = useCallback((chain: string) => {
if (chain) {
abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.chain,
value: chain,
});
setWithdrawAmount('');
}
}, []);
const onSelectToken = useCallback((token: TransferInputTokenResource) => {
if (token) {
abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.token,
value: token.address,
});
setWithdrawAmount('');
}
}, []);
const amountInputReceipt = [
{
key: 'freeCollateral',
label: (
<span>
{stringGetter({ key: STRING_KEYS.FREE_COLLATERAL })} <Tag>USDC</Tag>
</span>
),
value: (
<Styled.DiffOutput
type={OutputType.Fiat}
value={freeCollateral?.current}
newValue={freeCollateral?.postOrder}
sign={NumberSign.Negative}
hasInvalidNewValue={withdrawAmountBN.minus(freeCollateralBN).isNegative()}
withDiff={
Boolean(withdrawAmount) && !debouncedAmountBN.isNaN() && !debouncedAmountBN.isZero()
}
/>
),
},
];
const errorMessage = useMemo(() => {
if (error) {
return error?.message
? stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: { ERROR_MESSAGE: error.message },
})
: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG });
}
if (!toAddress) return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_ADDRESS });
if (debouncedAmountBN) {
if (!chainIdStr) {
return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_CHAIN });
} else if (!toToken) {
return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_ASSET });
}
}
if (MustBigNumber(debouncedAmountBN).gt(MustBigNumber(freeCollateralBN))) {
return stringGetter({ key: STRING_KEYS.WITHDRAW_MORE_THAN_FREE });
}
return undefined;
}, [error, freeCollateralBN, chainIdStr, debouncedAmountBN, toToken]);
const isDisabled =
!!errorMessage ||
!toToken ||
!chainIdStr ||
!toAddress ||
debouncedAmountBN.isNaN() ||
debouncedAmountBN.isZero();
return (
<Styled.Form onSubmit={onSubmit}>
<Styled.DestinationRow>
<FormInput
type={InputType.Text}
placeholder={stringGetter({ key: STRING_KEYS.ADDRESS })}
onChange={onChangeAddress}
value={toAddress || ''}
label={stringGetter({ key: STRING_KEYS.DESTINATION })}
/>
<ChainSelectMenu
label={stringGetter({ key: STRING_KEYS.NETWORK })}
selectedChain={chainIdStr || undefined}
onSelectChain={onSelectChain}
/>
</Styled.DestinationRow>
<TokenSelectMenu selectedToken={toToken || undefined} onSelectToken={onSelectToken} />
<Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}>
<FormInput
type={InputType.Number}
onChange={onChangeAmount}
value={withdrawAmount}
label={stringGetter({ key: STRING_KEYS.AMOUNT })}
slotRight={
<Styled.FormInputButton size={ButtonSize.XSmall} onClick={onClickMax}>
{stringGetter({ key: STRING_KEYS.MAX })}
</Styled.FormInputButton>
}
/>
</Styled.WithDetailsReceipt>
{errorMessage ? (
<AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>
) : (
transactionHash && (
<AlertMessage type={AlertType.Success}>
<Styled.TransactionInfo>
{stringGetter({ key: STRING_KEYS.WITHDRAW_IN_PROGRESS })}
</Styled.TransactionInfo>
</AlertMessage>
)
)}
<WithdrawButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
setSlippage={onSetSlippage}
slippage={slippage}
withdrawChain={chainIdStr || undefined}
withdrawToken={toToken || undefined}
/>
</Styled.Form>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.DiffOutput = styled(DiffOutput)`
--diffOutput-valueWithDiff-fontSize: 1em;
`;
Styled.Form = styled.form`
--form-input-height: 3.5rem;
min-height: calc(100% - var(--stickyArea0-bottomHeight));
${layoutMixins.flexColumn}
gap: 1.25rem;
${layoutMixins.stickyArea1}
`;
Styled.DestinationRow = styled.div`
${layoutMixins.spacedRow}
grid-template-columns: 1fr 1fr;
gap: 1rem;
`;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
`;
Styled.Link = styled(Link)`
color: var(--color-accent);
&:visited {
color: var(--color-accent);
}
`;
Styled.TransactionInfo = styled.span`
${layoutMixins.row}
`;
Styled.FormInputButton = styled(Button)`
${formMixins.inputInnerButton}
`;
@@ -0,0 +1,242 @@
import { useState } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components';
import { TransferInputTokenResource } from '@/constants/abacus';
import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { NumberSign } from '@/constants/numbers';
import { useStringGetter } from '@/hooks';
import { useAccountBalance } from '@/hooks/useAccountBalance';
import { layoutMixins } from '@/styles/layoutMixins';
import { Button } from '@/components/Button';
import { Details, DetailsItem } from '@/components/Details';
import { DiffOutput } from '@/components/DiffOutput';
import { Icon, IconName } from '@/components/Icon';
import { Output, OutputType } from '@/components/Output';
import { ToggleButton } from '@/components/ToggleButton';
import { WithReceipt } from '@/components/WithReceipt';
import { getSubaccount } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
import { SlippageEditor } from '../SlippageEditor';
type ElementProps = {
setSlippage: (slippage: number) => void;
slippage: number;
withdrawChain?: string;
withdrawToken?: TransferInputTokenResource;
isDisabled?: boolean;
isLoading?: boolean;
};
export const WithdrawButtonAndReceipt = ({
setSlippage,
slippage,
withdrawChain,
withdrawToken,
isDisabled,
isLoading,
}: ElementProps) => {
const [showFeeBreakdown, setShowFeeBreakdown] = useState(false);
const [isEditingSlippage, setIsEditingSlipapge] = useState(false);
const stringGetter = useStringGetter();
const { balance, queryStatus } = useAccountBalance({
addressOrDenom: withdrawToken?.address || undefined,
assetSymbol: withdrawToken?.symbol || undefined,
chainId: withdrawChain ? parseInt(withdrawChain) : undefined,
decimals: withdrawToken?.decimals || undefined,
isCosmosChain: false,
});
// TODO: uncomment when we have a way to get token amount estimate from abacus
// const balanceBN = MustBigNumber(balance);
// const newBalance =
// // toAmountMin && withdrawToken && parseUnits(toAmountMin, withdrawToken.decimals).toString();
const { leverage } = useSelector(getSubaccount, shallowEqual) || {};
const { summary, requestPayload } = useSelector(getTransferInputs, shallowEqual) || {};
const feeSubitems: DetailsItem[] = [];
if (typeof summary?.gasFee === 'number') {
feeSubitems.push({
key: 'gas-fees',
label: <span>{stringGetter({ key: STRING_KEYS.GAS_FEE })}</span>,
value: <Output type={OutputType.Fiat} value={summary?.gasFee} />,
});
}
if (typeof summary?.bridgeFee === 'number') {
feeSubitems.push({
key: 'bridge-fees',
label: <span>{stringGetter({ key: STRING_KEYS.BRIDGE_FEE })}</span>,
value: <Output type={OutputType.Fiat} value={summary?.bridgeFee} />,
});
}
const hasSubitems = feeSubitems.length > 0;
const showSubitemsToggle = showFeeBreakdown
? stringGetter({ key: STRING_KEYS.HIDE_ALL_DETAILS })
: stringGetter({ key: STRING_KEYS.SHOW_ALL_DETAILS });
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} />
),
subitems: feeSubitems,
},
{
key: 'leverage',
label: <span>{stringGetter({ key: STRING_KEYS.LEVERAGE })}</span>,
value: (
<Styled.DiffOutput
type={OutputType.Multiple}
value={leverage?.current}
newValue={leverage?.postOrder}
sign={NumberSign.Negative}
withDiff={Boolean(leverage?.current && leverage.current !== leverage?.postOrder)}
/>
),
},
{
key: 'exchange-rate',
label: <span>{stringGetter({ key: STRING_KEYS.EXCHANGE_RATE })}</span>,
value: withdrawToken && typeof summary?.exchangeRate === 'number' && (
<Styled.ExchangeRate>
<Output type={OutputType.Asset} value={1} fractionDigits={0} tag="USDC" />
=
<Output
type={OutputType.Asset}
value={summary?.exchangeRate}
tag={withdrawToken?.symbol}
/>
</Styled.ExchangeRate>
),
},
{
key: 'slippage',
label: <span>{stringGetter({ key: STRING_KEYS.SLIPPAGE })}</span>,
value: (
<SlippageEditor
slippage={slippage}
setIsEditing={setIsEditingSlipapge}
setSlippage={setSlippage}
/>
),
},
// TODO: uncomment when we have a way to get token amount estimate from abacus
// {
// key: 'wallet',
// label: (
// <span>
// {stringGetter({ key: STRING_KEYS.WALLET })}{' '}
// {withdrawToken && <Tag>{withdrawToken?.symbol}</Tag>}
// </span>
// ),
// value: (
// <Styled.DiffOutput
// type={OutputType.Asset}
// value={balanceBN?.toString()}
// newValue={newBalance}
// sign={NumberSign.Negative}
// withDiff={Boolean(balance !== newBalance)}
// />
// ),
// },
];
const isFormValid = !isDisabled && !isEditingSlippage && queryStatus !== 'error';
return (
<Styled.WithReceipt
slotReceipt={
<Styled.CollapsibleDetails>
<Styled.Details showSubitems={showFeeBreakdown} items={submitButtonReceipt} />
{hasSubitems && (
<Styled.DetailButtons>
<Styled.ToggleButton
shape={ButtonShape.Pill}
size={ButtonSize.XSmall}
isPressed={showFeeBreakdown}
onPressedChange={setShowFeeBreakdown}
slotLeft={<Icon iconName={IconName.Caret} />}
>
{showSubitemsToggle}
</Styled.ToggleButton>
</Styled.DetailButtons>
)}
</Styled.CollapsibleDetails>
}
>
<Button
action={ButtonAction.Primary}
type={ButtonType.Submit}
state={{
isDisabled: !isFormValid,
isLoading: (isFormValid && !requestPayload) || isLoading,
}}
>
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
</Button>
</Styled.WithReceipt>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.DiffOutput = styled(DiffOutput)`
--diffOutput-valueWithDiff-fontSize: 1em;
`;
Styled.ExchangeRate = styled.span`
${layoutMixins.row}
gap: 0.5ch;
`;
Styled.WithReceipt = styled(WithReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
`;
Styled.CollapsibleDetails = styled.div`
${layoutMixins.column}
padding: 0.375rem 1rem 0.5rem;
gap: 0.5rem;
`;
Styled.Details = styled(Details)`
font-size: 0.8125em;
`;
Styled.DetailButtons = styled.div`
${layoutMixins.spacedRow}
`;
Styled.ToggleButton = styled(ToggleButton)`
--button-toggle-off-backgroundColor: transparent;
--button-toggle-on-backgroundColor: transparent;
--button-toggle-on-textColor: var(--color-text-0);
svg {
width: 0.875em;
height: 0.875em;
}
&[data-state='on'] {
svg {
transform: rotate(180deg);
}
}
`;
-428
View File
@@ -1,428 +0,0 @@
import { type FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components';
import type { ChainData, CosmosChain, RouteData, TokenData } from '@0xsquid/sdk';
import { type NumberFormatValues } from 'react-number-format';
import debounce from 'lodash/debounce';
import { parseUnits } from 'ethers';
import { TransferInputField } from '@/constants/abacus';
import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons';
import { StringGetterFunction, STRING_KEYS } from '@/constants/localization';
import { NumberSign } from '@/constants/numbers';
import { CLIENT_NETWORK_CONFIGS, type DydxV4Network } from '@/constants/networks';
import { useAccounts, useDydxClient, useStringGetter } from '@/hooks';
import { useAccountBalance } from '@/hooks/useAccountBalance';
import { layoutMixins } from '@/styles/layoutMixins';
import { AlertMessage } from '@/components/AlertMessage';
import { Button } from '@/components/Button';
import { DiffOutput } from '@/components/DiffOutput';
import { FormInput } from '@/components/FormInput';
import { InputType } from '@/components/Input';
import { Link } from '@/components/Link';
import { OutputType } from '@/components/Output';
import { Tag } from '@/components/Tag';
import { WithLabel } from '@/components/WithLabel';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import abacusStateManager from '@/lib/abacus';
import { convertBech32Address } from '@/lib/addressUtils';
import { MustBigNumber } from '@/lib/numbers';
import squidRouter from '@/lib/squidRouter';
import { useSquidData } from './DepositForm/useSquidData';
import { ChainSelectMenu } from './DepositForm/ChainSelectMenu';
import { DepositButtonAndReceipt } from './DepositForm/DepositButtonAndReceipt';
import { TokenSelectMenu } from './DepositForm/TokenSelectMenu';
import { log } from '@/lib/telemetry';
import { getSelectedNetwork } from '@/state/appSelectors';
type DepositFormProps = {
onDeposit?: () => void;
onError?: () => void;
};
// debounced function should be outside our functional component
const debouncedGetRoute = debounce(
async ({
amount,
currentChain,
cosmosAddress,
fromAmount,
setAbacusTransferInput,
setError,
setIsLoading,
setSquidRoute,
slippage,
sourceToken,
}) => {
if (!currentChain || !sourceToken || !fromAmount || !cosmosAddress) return;
setIsLoading(true);
setSquidRoute(undefined);
setError(undefined);
const params = {
fromChain: currentChain.chainId,
fromToken: sourceToken.address,
fromAmount: parseUnits(amount, sourceToken.decimals).toString(),
toAddress: convertBech32Address({ address: cosmosAddress, bech32Prefix: 'osmo' }), // the recipient of the trade
slippage: slippage * 100,
...squidRouter.SQUID_ROUTE_DEFAULTS,
};
try {
const { route } = await squidRouter.getRoute(params);
setSquidRoute(route);
setAbacusTransferInput(route.estimate.toAmountUSD);
} catch (error) {
log('DepositForm/getRoute', error);
setError(error);
} finally {
setIsLoading(false);
}
},
1000,
{ trailing: true }
);
const getDepositFormErrorMessage = ({
error,
balance,
currentChain,
fromAmount,
sourceToken,
stringGetter,
}: {
error?: any;
balance?: string;
currentChain?: ChainData;
fromAmount: string;
sourceToken?: TokenData;
stringGetter: StringGetterFunction;
}): string | undefined => {
if (error) {
return error?.message
? stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: { ERROR_MESSAGE: error.message },
})
: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG });
}
if (fromAmount) {
if (!currentChain) {
return stringGetter({ key: STRING_KEYS.MUST_SPECIFY_CHAIN });
} else if (!sourceToken) {
return stringGetter({ key: STRING_KEYS.MUST_SPECIFY_ASSET });
}
}
if (MustBigNumber(fromAmount).gt(MustBigNumber(balance))) {
return stringGetter({ key: STRING_KEYS.DEPOSIT_MORE_THAN_BALANCE });
}
return undefined;
};
export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const stringGetter = useStringGetter();
const { dydxAddress: cosmosAddress, signerWagmi, signerGraz } = useAccounts();
// const { networkConfig } = useDydxClient();
const selectedNetwork = useSelector(getSelectedNetwork);
const evmChainId = Number(CLIENT_NETWORK_CONFIGS[selectedNetwork as DydxV4Network].ethereumChainId);
const { chains, tokens } = useSquidData(signerGraz ? 'cosmos' : 'evm');
// User inputs
const defaultChain: ChainData | undefined = useMemo(
() => chains?.find(({ chainId }) => chainId === evmChainId) ?? undefined,
[chains]
);
const [currentChain, setCurrentChain] = useState<ChainData | undefined>(defaultChain);
const defaultToken: TokenData | undefined = useMemo(
() => tokens?.filter(({ chainId }) => chainId === currentChain?.chainId)?.[0] ?? undefined,
[currentChain]
);
const [sourceToken, setSourceToken] = useState<TokenData | undefined>(defaultToken);
const [fromAmount, setFromAmount] = useState('');
const [slippage, setSlippage] = useState(0.001); // 0.1% slippage
// Async Data
const [squidRoute, setSquidRoute] = useState<RouteData>();
const [transactionHash, setTransactionHash] = useState<string | undefined>();
// Form states
const [error, setError] = useState<Error | undefined>();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setCurrentChain(defaultChain);
return () => {
abacusStateManager.clearTransferInputValues();
};
}, [defaultChain]);
useEffect(() => {
setSourceToken(defaultToken);
setSquidRoute(undefined);
setFromAmount('');
}, [currentChain]);
// @ts-ignore ibcDenom exists but is not typed properly on 0xSquid SDK
const addressOrDenom = sourceToken?.ibcDenom || sourceToken?.address;
const bech32AddrPrefix = (currentChain as CosmosChain)?.bech32Config?.bech32PrefixAccAddr;
const { balance, isBalanceError, isBalanceLoading } = useAccountBalance({
addressOrDenom,
assetSymbol: sourceToken?.symbol,
bech32AddrPrefix,
chainId: currentChain?.chainId,
decimals: sourceToken?.decimals,
rpc: currentChain?.rpc,
isCosmosChain: currentChain?.chainType === 'cosmos',
});
// BN
const fromAmountBN = MustBigNumber(fromAmount);
const balanceBN = MustBigNumber(balance);
const requestDeposit = async () => {
if (!squidRoute) {
log('DepositForm/requestDeposit', new Error('No squid route'));
return;
}
try {
setIsLoading(true);
setError(undefined);
setTransactionHash(undefined);
const txReceipt = await squidRouter.executeRoute({
route: squidRoute,
signer: signerGraz || signerWagmi,
});
console.log('DepositForm/executeRoute', { txReceipt });
setFromAmount('');
setTransactionHash(txReceipt?.transactionHash);
} catch (error) {
log('DepositForm/executeRoute', error);
setError(error);
} finally {
setIsLoading(false);
}
};
const setAbacusTransferInput = (value: string | number) => {
abacusStateManager.setTransferValue({ value, field: TransferInputField.usdcSize });
};
const getRoute = useCallback(
({ newAmount, newSlippage }: { newAmount: string; newSlippage?: number }) => {
debouncedGetRoute.cancel();
if (MustBigNumber(newAmount).gt(0) && MustBigNumber(newAmount).lte(balanceBN)) {
debouncedGetRoute({
amount: newAmount,
currentChain,
cosmosAddress,
fromAmount,
setAbacusTransferInput,
setError,
setIsLoading,
setSquidRoute,
slippage: newSlippage || slippage,
sourceToken,
});
} else {
setSquidRoute(undefined);
setAbacusTransferInput(0);
}
},
[currentChain, sourceToken, fromAmount, slippage]
);
const onChangeAmount = ({ value }: NumberFormatValues) => {
setFromAmount(value);
getRoute({ newAmount: value });
};
const onSetSlippage = (newSlippage: number) => {
setSlippage(newSlippage);
if (MustBigNumber(newSlippage).gt(0) && fromAmountBN.gt(0)) {
getRoute({ newAmount: fromAmount, newSlippage });
}
};
const onClickMax = () => {
if (balance) {
setFromAmount(balanceBN.toString());
}
};
const amountInputReceipt = [
{
key: 'amount',
label: (
<span>
{stringGetter({ key: STRING_KEYS.AVAILABLE })}{' '}
{sourceToken && <Tag>{sourceToken.symbol}</Tag>}
</span>
),
value: (
<DiffOutput
type={OutputType.Asset}
value={balance}
newValue={balanceBN.minus(fromAmountBN).toString()}
sign={NumberSign.Negative}
hasInvalidNewValue={balanceBN.minus(fromAmountBN).isNegative()}
withDiff={Boolean(fromAmount && balance) && !fromAmountBN.isNaN()}
/>
),
},
];
const errorMessage = getDepositFormErrorMessage({
error,
balance,
currentChain,
fromAmount,
sourceToken,
stringGetter,
});
const isDisabled =
Boolean(errorMessage) ||
!sourceToken ||
!currentChain ||
fromAmountBN.isNaN() ||
fromAmountBN.isZero() ||
!squidRoute;
const alertMessage = {
type: errorMessage ? AlertType.Error : AlertType.Info,
message:
errorMessage ||
(transactionHash && (
<span>
{stringGetter({ key: STRING_KEYS.VIEW_DETAILS })}
{' on '}
<Styled.Link href={`https://testnet.axelarscan.io/transfer/${transactionHash}`}>
Axelar
</Styled.Link>
.
</span>
)),
};
return (
<Styled.Form
onSubmit={(e: FormEvent) => {
e.preventDefault();
requestDeposit();
}}
>
<ChainSelectMenu
chains={chains}
currentChain={currentChain}
setCurrentChain={setCurrentChain}
/>
<TokenSelectMenu
tokens={tokens}
chain={currentChain}
sourceToken={sourceToken}
setSourceToken={(token: TokenData) => {
setSourceToken(token);
if (fromAmount) {
debouncedGetRoute({
amount: fromAmount,
currentChain,
cosmosAddress,
fromAmount,
setError,
setIsLoading,
setSquidRoute,
slippage,
sourceToken: token,
});
} else {
setSquidRoute(undefined);
}
}}
/>
<WithLabel label={stringGetter({ key: STRING_KEYS.AMOUNT })}>
<Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}>
<Styled.FormInput
type={InputType.Number}
onChange={onChangeAmount}
value={fromAmount}
slotRight={
<Button size={ButtonSize.XSmall} onClick={onClickMax}>
{stringGetter({ key: STRING_KEYS.MAX })}
</Button>
}
/>
</Styled.WithDetailsReceipt>
</WithLabel>
{alertMessage.message && (
<Styled.AlertMessage type={alertMessage.type}>{alertMessage.message}</Styled.AlertMessage>
)}
<Styled.Footer>
<DepositButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading || isBalanceLoading}
chainId={currentChain?.chainId}
setError={setError}
setSlippage={onSetSlippage}
slippage={slippage}
sourceToken={sourceToken}
squidRoute={squidRoute}
/>
</Styled.Footer>
</Styled.Form>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Form = styled.form`
${layoutMixins.flexColumn}
gap: 1.5rem;
${layoutMixins.stickyArea1}
min-height: calc(100% - var(--stickyArea0-bottomHeight));
`;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
`;
Styled.FormInput = styled(FormInput)`
--formInput-backgroundColor: var(--color-layer-4);
--formInput-input-height: 3.375rem;
border: 1px solid var(--color-layer-6);
`;
Styled.Link = styled(Link)`
color: var(--color-accent);
`;
Styled.AlertMessage = styled(AlertMessage)`
cursor: normal;
user-select: none;
margin: 0;
`;
Styled.Footer = styled.footer`
${layoutMixins.stickyFooter}
margin-top: auto;
`;
@@ -1,89 +0,0 @@
import type { ChainData, TokenData } from '@0xsquid/sdk';
import styled, { type AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
import { useStringGetter } from '@/hooks';
import { SearchSelectMenu } from '@/components/SearchSelectMenu';
import { Tag } from '@/components/Tag';
import { WithLabel } from '@/components/WithLabel';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
chain?: ChainData;
tokens?: TokenData[];
sourceToken?: TokenData;
setSourceToken: (token: TokenData) => void;
};
export const TokenSelectMenu = ({
tokens = [],
chain,
sourceToken,
setSourceToken,
}: ElementProps) => {
const stringGetter = useStringGetter();
const tokenItems = Object.values(tokens)
.filter((token: TokenData) => token.chainId === chain?.chainId)
.map((token: TokenData) => ({
value: token.address,
label: token.name,
onSelect: () => setSourceToken(token),
slotBefore: <Styled.Img src={token.logoURI} alt="" />,
}));
return (
<SearchSelectMenu
disabled={!chain}
items={[
{
group: 'assets',
groupLabel: stringGetter({ key: STRING_KEYS.ASSET }),
items: tokenItems,
},
]}
label={stringGetter({ key: STRING_KEYS.ASSET })}
withReceiptItems={[
{
key: 'swap',
label: stringGetter({ key: STRING_KEYS.SWAP }),
value: sourceToken && (
<>
<Tag>{sourceToken?.symbol}</Tag><Tag>USDC</Tag>
</>
),
tooltip: 'swap',
withTooltipIcon: true,
},
]}
>
<Styled.AssetRow>
{sourceToken ? (
<>
<Styled.Img src={sourceToken?.logoURI} alt="" /> {sourceToken?.name}{' '}
<Tag>{sourceToken?.symbol}</Tag>
</>
) : (
stringGetter({ key: STRING_KEYS.SELECT_ASSET })
)}
</Styled.AssetRow>
</SearchSelectMenu>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.AssetRow = styled.div`
${layoutMixins.row}
gap: 0.5rem;
color: var(--color-text-2);
font: var(--font-base-book);
`;
Styled.Img = styled.img`
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
`;
@@ -1,33 +0,0 @@
import { useEffect, useState } from 'react';
import type { ChainData, TokenData } from '@0xsquid/sdk';
import squidRouter from '@/lib/squidRouter';
import { WAGMI_SUPPORTED_CHAINS } from '@/lib/wagmi';
/**
* @description hook to return chains and tokens supported by squid
* @note Limit to EVM chains | Cosmos chains can only be used as destination on 0xSquid as of 02/21/2023
*/
export const useSquidData = (selectedChainType: 'cosmos' | 'evm') => {
const [, setHasInitialized] = useState(false);
useEffect(() => {
(async () => {
await squidRouter.initializeClient();
setHasInitialized(true);
})();
}, []);
const chainData: ChainData[] = squidRouter.getChains();
const tokenData: TokenData[] = squidRouter.getTokens();
const wagmiChainIds = Object.fromEntries(
WAGMI_SUPPORTED_CHAINS.map((supportedChain) => [supportedChain.id, supportedChain])
);
return {
chains: chainData.filter(({ chainId, chainType }) =>
selectedChainType === 'evm' ? wagmiChainIds[chainId] : chainType === 'cosmos'
),
tokens: tokenData,
};
};
@@ -1,7 +1,7 @@
import { useState } from 'react';
import type { Story } from '@ladle/react';
import styled, { AnyStyledComponent } from 'styled-components';
import { OrderSide } from '@dydxprotocol/v4-client';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { PositionSide } from '@/constants/trade';
import { breakpoints } from '@/styles';
+3 -7
View File
@@ -2,7 +2,7 @@ import { type Dispatch, type SetStateAction, 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';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { type Nullable, TradeInputField } from '@/constants/abacus';
import { PositionSide } from '@/constants/trade';
@@ -43,9 +43,7 @@ export const LeverageSlider = ({
},
[PositionSide.Long]: {
min:
orderSide === OrderSide.BUY
? leverageBN.toNumber()
: maxLeverageBN.negated().toNumber(),
orderSide === OrderSide.BUY ? leverageBN.toNumber() : maxLeverageBN.negated().toNumber(),
max: orderSide === OrderSide.BUY ? maxLeverageBN.toNumber() : leverageBN.toNumber(),
midpoint:
orderSide === OrderSide.SELL
@@ -56,9 +54,7 @@ export const LeverageSlider = ({
},
[PositionSide.Short]: {
min:
orderSide === OrderSide.BUY
? leverageBN.toNumber()
: maxLeverageBN.negated().toNumber(),
orderSide === OrderSide.BUY ? leverageBN.toNumber() : maxLeverageBN.negated().toNumber(),
max: orderSide === OrderSide.BUY ? maxLeverageBN.toNumber() : leverageBN.toNumber(),
midpoint:
orderSide === OrderSide.BUY
@@ -1,7 +1,7 @@
import type { Dispatch, SetStateAction } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import styled, { AnyStyledComponent } from 'styled-components';
import { OrderSide } from '@dydxprotocol/v4-client';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { TradeInputField, Nullable } from '@/constants/abacus';
import { ButtonShape } from '@/constants/buttons';
@@ -1,5 +1,5 @@
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { OrderSide } from '@dydxprotocol/v4-client';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import type { TradeInputSummary } from '@/constants/abacus';
import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons';
@@ -1,7 +1,7 @@
import { memo } from 'react';
import styled, { css } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import { OrderSide } from '@dydxprotocol/v4-client';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { AbacusOrderSide } from '@/constants/abacus';
import { ButtonShape, ButtonSize } from '@/constants/buttons';
@@ -31,9 +31,7 @@ export const TradeSideToggle = memo(() => {
onValueChange={(side: OrderSide) => {
abacusStateManager.setTradeValue({
value:
side === OrderSide.BUY
? AbacusOrderSide.buy.rawValue
: AbacusOrderSide.sell.rawValue,
side === OrderSide.BUY ? AbacusOrderSide.buy.rawValue : AbacusOrderSide.sell.rawValue,
field: TradeInputField.side,
});
}}
+4 -4
View File
@@ -5,14 +5,14 @@ import { shallowEqual, useSelector } from 'react-redux';
import type { SyntheticInputEvent } from 'react-number-format/types/types';
import { debounce } from 'lodash';
import { StdFee } from '@cosmjs/stargate';
import { validation } from '@dydxprotocol/v4-client';
import { validation } from '@dydxprotocol/v4-client-js';
import { TransferInputField, TransferType } from '@/constants/abacus';
import { AlertType } from '@/constants/alerts';
import { ButtonShape, ButtonSize } from '@/constants/buttons';
import { ERROR_STRING_KEYS } from '@/constants/localization/errors';
import { STRING_KEYS } from '@/constants/localization';
import { CHAIN_IDS } from '@/constants/networks';
import { CLIENT_NETWORK_CONFIGS } from '@/constants/networks';
import { NumberSign, QUANTUM_MULTIPLIER } from '@/constants/numbers';
import { DYDX_CHAIN_ASSET_COIN_DENOM, DYDX_CHAIN_INFO, DydxChainAsset } from '@/constants/wallets';
@@ -228,7 +228,7 @@ export const TransferForm = ({
const networkOptions = [
{
chainId: CHAIN_IDS[selectedNetwork],
chainId: CLIENT_NETWORK_CONFIGS[selectedNetwork].dydxChainId,
label: (
<Styled.InlineRow>
<AssetIcon symbol="DYDX" /> {stringGetter({ key: STRING_KEYS.DYDX_CHAIN })}
@@ -318,7 +318,7 @@ export const TransferForm = ({
/>
<Styled.NetworkSelectMenu
label={stringGetter({ key: STRING_KEYS.NETWORK })}
value={CHAIN_IDS[selectedNetwork]}
value={CLIENT_NETWORK_CONFIGS[selectedNetwork].dydxChainId}
slotTriggerAfter={null}
>
{networkOptions.map(({ chainId, label }) => (
+8 -1
View File
@@ -1,6 +1,7 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { useSelectedNetwork } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { headerMixins } from '@/styles/headerMixins';
import { DropdownSelectMenu } from '@/components/DropdownSelectMenu';
@@ -18,7 +19,6 @@ export const NetworkSelectMenu = ({ align, sideOffset }: StyleProps) => {
return (
<Styled.DropdownSelectMenu
disabled={import.meta.env.MODE === 'production'}
items={networks}
value={selectedNetwork}
onValueChange={switchNetwork}
@@ -32,4 +32,11 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.DropdownSelectMenu = styled(DropdownSelectMenu)`
${headerMixins.dropdownTrigger}
& > span:first-of-type {
${layoutMixins.textOverflow}
max-width: 5.625rem;
min-width: 0;
white-space: nowrap;
}
`;
+11 -43
View File
@@ -1,45 +1,13 @@
import { type MenuItem } from '@/constants/menus';
import { type DydxNetwork, DydxV3Network, DydxV4Network } from '@/constants/networks';
import { type DydxNetwork, NETWORK_ENDPOINTS } from '@/constants/networks';
import { useStringGetter } from '@/hooks';
export const useNetworks = (): MenuItem<DydxNetwork>[] =>
import.meta.env.MODE === 'production'
? [
{
value: DydxV4Network.V4Testnet2,
label: 'Testnet',
slotBefore: <b>v4</b>,
},
]
: [
{
value: DydxV4Network.V4Mainnet,
label: 'Mainnet',
slotBefore: <b>v4</b>,
disabled: true,
},
{
value: DydxV4Network.V4Staging,
label: 'Staging',
slotBefore: <b>v4</b>,
},
{
value: DydxV4Network.V4Testnet2,
label: 'Testnet-2',
slotBefore: <b>v4</b>,
},
{
value: DydxV4Network.V4Local,
label: 'Dev',
slotBefore: <b>v4</b>,
},
{
value: DydxV3Network.V3Mainnet,
label: 'Mainnet',
slotBefore: <b>v3</b>,
},
{
value: DydxV3Network.V3Testnet,
label: 'Testnet',
slotBefore: <b>v3</b>,
},
];
export const useNetworks = (): MenuItem<DydxNetwork>[] => {
const stringGetter = useStringGetter();
return NETWORK_ENDPOINTS.environments.map(({ stringKey, environment }) => ({
key: environment,
label: stringGetter({ key: stringKey }),
value: environment,
}));
};
+1 -1
View File
@@ -1,7 +1,7 @@
import { useEffect } from 'react';
import styled, { type AnyStyledComponent, css } from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { OrderSide } from '@dydxprotocol/v4-client';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import type { ColumnSize } from '@react-types/table';
import { type Asset, type SubaccountFill } from '@/constants/abacus';
+1 -1
View File
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import { OrderSide } from '@dydxprotocol/v4-client';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { MarketTrade } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
+1 -1
View File
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components';
import { OrderSide } from '@dydxprotocol/v4-client';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { type OrderbookLine, TradeInputField } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
+1 -1
View File
@@ -2,7 +2,7 @@ import { useEffect } from 'react';
import styled, { css, type AnyStyledComponent } from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { DateTime } from 'luxon';
import { OrderSide } from '@dydxprotocol/v4-client';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { ColumnSize } from '@react-types/table';
import type { Dispatch } from '@reduxjs/toolkit';