diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7f94ab..1c17717 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15836,6 +15836,12 @@ packages: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: true + file:../../abacus/build/packages/js/dydxprotocol-abacus-0.2.28.tgz: + resolution: {integrity: sha512-aOT+QROV5aL0QLNq7QINW6Bw3Vz8ds7LOoC4MQfPDsGGqgyundpAcPkLmgcbIk0Ft8BQsdOz+HbbAYL4n7Iqlg==, tarball: file:../../abacus/build/packages/js/dydxprotocol-abacus-0.2.28.tgz} + name: '@dydxprotocol/abacus' + version: 0.2.28 + dev: false + github.com/release-it/conventional-changelog/a4acfeac76369937a6566d698a5ae9180eceb413(release-it@15.11.0): resolution: {tarball: https://codeload.github.com/release-it/conventional-changelog/tar.gz/a4acfeac76369937a6566d698a5ae9180eceb413} id: github.com/release-it/conventional-changelog/a4acfeac76369937a6566d698a5ae9180eceb413 diff --git a/src/App.tsx b/src/App.tsx index 1869616..db877bb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 = () => ( - + + + diff --git a/src/components/SearchSelectMenu.tsx b/src/components/SearchSelectMenu.tsx index 1fd1ef7..3a810d4 100644 --- a/src/components/SearchSelectMenu.tsx +++ b/src/components/SearchSelectMenu.tsx @@ -19,6 +19,7 @@ type ElementProps = { disabled?: boolean; label?: string; items: MenuConfig; + slotEmpty?: ReactNode; withSearch?: boolean; withReceiptItems?: DetailsItem[]; }; @@ -52,12 +53,10 @@ export const SearchSelectMenu = ({ const Trigger = asChild ? ( children ) : ( - <> - - {children} - + + {label ? {children} : children} - + ); return ( @@ -86,7 +85,7 @@ export const SearchSelectMenu = ({ const Styled: Record = {}; 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; } diff --git a/src/constants/abacus.ts b/src/constants/abacus.ts index a769f95..af005ff 100644 --- a/src/constants/abacus.ts +++ b/src/constants/abacus.ts @@ -12,6 +12,10 @@ export type AbacusDydxChainQueriesProtocol = Omit< Abacus.exchange.dydx.abacus.protocols.DYDXChainQueriesProtocol, '__doNotUseOrImplementIt' >; +export type AbacusDYDXChainTransactionsProtocol = Omit< + Abacus.exchange.dydx.abacus.protocols.DYDXChainTransactionsProtocol, + '__doNotUseOrImplementIt' +>; export type AbacusRestProtocol = Omit< Abacus.exchange.dydx.abacus.protocols.RestProtocol, '__doNotUseOrImplementIt' @@ -44,6 +48,10 @@ export const QueryType = Abacus.exchange.dydx.abacus.protocols.QueryType; const queryTypes = [...QueryType.values()] as const; export type QueryTypes = (typeof queryTypes)[number]; +export const TransactionType = Abacus.exchange.dydx.abacus.protocols.TransactionType; +const TransactionTypes = [...TransactionType.values()] as const; +export type TransactionTypes = (typeof TransactionTypes)[number]; + export type NetworkConfig = { chainId: string; indexerUrl: string; @@ -98,6 +106,8 @@ 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 const ErrorType = Abacus.exchange.dydx.abacus.output.input.ErrorType; export const InputSelectionOption = Abacus.exchange.dydx.abacus.output.input.SelectionOption; diff --git a/src/constants/dialogs.ts b/src/constants/dialogs.ts index 5655452..ed20d3f 100644 --- a/src/constants/dialogs.ts +++ b/src/constants/dialogs.ts @@ -12,6 +12,7 @@ export enum DialogTypes { Receive = 'Receive', Trade = 'Trade', Transfer = 'Transfer', + Withdraw = 'Withdraw', } export enum TradeBoxDialogTypes { diff --git a/src/hooks/index.ts b/src/hooks/index.ts index bc0c3cd..950dc76 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -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'; @@ -17,6 +18,7 @@ import { useOnClickOutside } from './useOnClickOutside'; import { usePageTitlePriceUpdates } from './usePageTitlePriceUpdates'; import { useShouldShowFooter } from './useShouldShowFooter'; import { useSelectedNetwork } from './useSelectedNetwork'; +import { useSquidRouter } from './useSquidRouter'; import { useStringGetter } from './useStringGetter'; import { useSubaccount } from './useSubaccount'; @@ -25,6 +27,7 @@ export { useBreakpoints, useCommandMenu, useCurrentMarketId, + useDebounce, useDocumentTitle, useDydxClient, useAccountBalance, @@ -41,5 +44,6 @@ export { useShouldShowFooter, useSelectedNetwork, useStringGetter, + useSquidRouter, useSubaccount, }; diff --git a/src/hooks/useAccountBalance.ts b/src/hooks/useAccountBalance.ts index 1885b90..ea9269a 100644 --- a/src/hooks/useAccountBalance.ts +++ b/src/hooks/useAccountBalance.ts @@ -3,7 +3,7 @@ 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 { QUANTUM_MULTIPLIER } from '@/constants/numbers'; @@ -43,7 +43,7 @@ export const useAccountBalance = ({ assetSymbol, bech32AddrPrefix, chainId, - decimals, + decimals = 0, rpc, isCosmosChain, }: UseAccountBalanceProps = {}) => { @@ -66,7 +66,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 +76,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 +102,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', }; }; diff --git a/src/hooks/useAccounts.tsx b/src/hooks/useAccounts.tsx index feae33a..c1ce52f 100644 --- a/src/hooks/useAccounts.tsx +++ b/src/hooks/useAccounts.tsx @@ -63,6 +63,8 @@ const useAccountsContext = () => { forgetEvmSignature(previousEvmAddress); } + if (evmAddress) abacusStateManager.setEvmAddress(evmAddress); + setPreviousEvmAddress(evmAddress); }, [evmAddress]); diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 0000000..5b38cc4 --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -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(value: T, delay?: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebouncedValue(value), delay || 500); + + return () => { + clearTimeout(timer); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/src/hooks/useInitializePage.ts b/src/hooks/useInitializePage.ts index f7f41dd..b9c66a6 100644 --- a/src/hooks/useInitializePage.ts +++ b/src/hooks/useInitializePage.ts @@ -6,7 +6,6 @@ import { LocalStorageKey } from '@/constants/localStorage'; import { DEFAULT_APP_ENVIRONMENT, DydxNetwork, - isDydxV4Network, isValidDydxNetwork, } from '@/constants/networks'; @@ -16,8 +15,6 @@ import { initializeLocalization } from '@/state/app'; import abacusStateManager from '@/lib/abacus'; -import squidRouter from '@/lib/squidRouter'; - export const useInitializePage = () => { const dispatch = useDispatch(); @@ -31,9 +28,5 @@ export const useInitializePage = () => { useEffect(() => { dispatch(initializeLocalization()); abacusStateManager.start({ network: localStorageNetwork }); - - if (isDydxV4Network(localStorageNetwork)) { - squidRouter.switchNetwork(localStorageNetwork); - } }, []); }; diff --git a/src/hooks/useSquidRouter.tsx b/src/hooks/useSquidRouter.tsx new file mode 100644 index 0000000..8cd0cf8 --- /dev/null +++ b/src/hooks/useSquidRouter.tsx @@ -0,0 +1,118 @@ +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { ethers } from 'ethers'; +import { useSelector } from 'react-redux'; +import { Squid, type GetRoute, type RouteResponse, ChainData, ChainType } from '@0xsquid/sdk'; +import { USDC_DENOM } from '@dydxprotocol/v4-client'; + +import { TransferInputs } from '@/constants/abacus'; +import { DydxV4Network, isDydxV4Network } from '@/constants/networks'; +import { useAccounts } from '@/hooks/useAccounts'; + +import { getSelectedNetwork } from '@/state/appSelectors'; + +const SQUID_BASE_URL: Record = { + [DydxV4Network.V4Testnet2]: 'https://squid-api-git-feat-cosmos-maintestnet-0xsquid.vercel.app', + [DydxV4Network.V4Staging]: undefined, + [DydxV4Network.V4Local]: undefined, + [DydxV4Network.V4Mainnet]: 'https://api.0xsquid.com', +}; + +/** + * @description Deposit route defaults for IBC Testnet + * */ +export const SQUID_DEPOSIT_ROUTE_DEFAULTS = { + toChain: 'dydx-testnet-2', + toToken: USDC_DENOM, + enableForecall: false, // instant execution service, defaults to true +}; + +export const SQUID_WITHDRAW_ROUTE_DEFAULTS = { + fromChain: 'dydx-testnet-2', + fromToken: USDC_DENOM, + enableForecall: false, // instant execution service, defaults to true +}; + +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; +const SquidContext = createContext(undefined); +SquidContext.displayName = '0xSquid'; + +export const SquidProvider = ({ ...props }) => ( + +); + +export const useSquid = () => useContext(SquidContext); + +export const useSquidRouter = () => { + const squid = useSquid(); + const { signerWagmi } = useAccounts(); + + const submitDeposit = useCallback( + async (requestPayload: TransferInputs['requestPayload']) => { + 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'); + } + 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, + }; + return await signerWagmi.sendTransaction(tx); + }, + [signerWagmi] + ); + + const getStatus = useCallback( + async (transactionId: string) => { + const status = await squid?.getStatus({ transactionId }); + return status; + }, + [squid] + ); + + return { + getStatus, + submitDeposit, + + axelarscanURL: squid?.axelarscanURL, + }; +}; diff --git a/src/hooks/useSubaccount.tsx b/src/hooks/useSubaccount.tsx index 2ce92c5..2a5888c 100644 --- a/src/hooks/useSubaccount.tsx +++ b/src/hooks/useSubaccount.tsx @@ -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 { EncodeObject, type Coin } from '@cosmjs/proto-signing'; import { Method } from '@cosmjs/tendermint-rpc'; import { @@ -301,6 +301,32 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo return result; }, + + sendSquidWithdraw: async ({ + subaccount, + payload, + }:{ + subaccount: SubaccountClient; + payload: string; + }) => { + const decode = (str: string):string => Buffer.from(str, 'base64').toString('binary'); + const decoded = decode(payload); + + const json = JSON.parse(decoded); + + const ibcMsg: EncodeObject = { + typeUrl: json.msgTypeUrl, + value: json.msg, + }; + const encodeObjects: Promise = Promise.resolve([ibcMsg]); + + await compositeClient?.send( + subaccount.wallet, + () => encodeObjects, + false + ) + }, + }), [compositeClient] ); diff --git a/src/layout/DialogManager.tsx b/src/layout/DialogManager.tsx index 740e2a2..e647bc2 100644 --- a/src/layout/DialogManager.tsx +++ b/src/layout/DialogManager.tsx @@ -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]: , [DialogTypes.Trade]: , [DialogTypes.Transfer]: , + [DialogTypes.Withdraw]: , }[type]; }; diff --git a/src/lib/abacus/index.ts b/src/lib/abacus/index.ts index 1d43a86..939b96e 100644 --- a/src/lib/abacus/index.ts +++ b/src/lib/abacus/index.ts @@ -147,6 +147,10 @@ class AbacusStateManager { this.stateManager.accountAddress = walletAddress; }; + setEvmAddress = (evmAddress: string) => { + this.stateManager.sourceAddress = evmAddress; + }; + setSubaccountNumber = (subaccountNumber: number) => (this.stateManager.subaccountNumber = subaccountNumber); diff --git a/src/lib/squidRouter.ts b/src/lib/squidRouter.ts deleted file mode 100644 index 67cd35f..0000000 --- a/src/lib/squidRouter.ts +++ /dev/null @@ -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.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 => { - 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; diff --git a/src/lib/wagmi.ts b/src/lib/wagmi.ts index 0c2c35d..1bef169 100644 --- a/src/lib/wagmi.ts +++ b/src/lib/wagmi.ts @@ -38,6 +38,10 @@ export const WAGMI_SUPPORTED_CHAINS: Chain[] = [ optimismGoerli, ]; +export const WAGMI_SUPPORTED_CHAIN_MAP = Object.fromEntries( + WAGMI_SUPPORTED_CHAINS.map((supportedChain) => [supportedChain.id, supportedChain]) +); + const { chains, publicClient, webSocketPublicClient } = configureChains(WAGMI_SUPPORTED_CHAINS, [ alchemyProvider({ apiKey: import.meta.env.VITE_ALCHEMY_API_KEY }), jsonRpcProvider({ @@ -162,7 +166,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 +184,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); }; diff --git a/src/state/appMiddleware.ts b/src/state/appMiddleware.ts index 23b5258..c48c494 100644 --- a/src/state/appMiddleware.ts +++ b/src/state/appMiddleware.ts @@ -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) case setSelectedNetwork.type: { store.dispatch(resetPerpetualsState()); abacusStateManager.switchNetwork(payload); - - if (isDydxV4Network(payload)) { - squidRouter.switchNetwork(payload); - } break; } case setApiState.type: { diff --git a/src/styles/popoverMixins.ts b/src/styles/popoverMixins.ts index 47932cf..17be685 100644 --- a/src/styles/popoverMixins.ts +++ b/src/styles/popoverMixins.ts @@ -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); `, diff --git a/src/views/AccountInfo/AccountInfoConnectedState.tsx b/src/views/AccountInfo/AccountInfoConnectedState.tsx index d04be9a..3af94e9 100644 --- a/src/views/AccountInfo/AccountInfoConnectedState.tsx +++ b/src/views/AccountInfo/AccountInfoConnectedState.tsx @@ -72,16 +72,24 @@ export const AccountInfoConnectedState = () => { {!showHeader ? null : ( {stringGetter({ key: STRING_KEYS.ACCOUNT })} - - dispatch(openDialog({ type: DialogTypes.Deposit }))} - shape={ButtonShape.Pill} - size={ButtonSize.XSmall} - slotLeft={} - > - {stringGetter({ key: STRING_KEYS.MANAGE_FUNDS })} - + + dispatch(openDialog({ type: DialogTypes.Withdraw }))} + shape={ButtonShape.Pill} + size={ButtonSize.XSmall} + > + {stringGetter({ key: STRING_KEYS.WITHDRAW })} + + dispatch(openDialog({ type: DialogTypes.Deposit }))} + shape={ButtonShape.Pill} + size={ButtonSize.XSmall} + > + {stringGetter({ key: STRING_KEYS.DEPOSIT })} + + )} @@ -273,6 +281,11 @@ Styled.Header = styled.header` padding: 0 1.25rem; `; +Styled.TransferButtons = styled.div` + ${layoutMixins.row} + gap: 1rem; +` + Styled.ConnectedAccountInfoContainer = styled.div<{ $showHeader?: boolean }>` ${layoutMixins.column} ${layoutMixins.withOuterAndInnerBorders} diff --git a/src/views/dialogs/DepositDialog.tsx b/src/views/dialogs/DepositDialog.tsx index 43cee62..9baa069 100644 --- a/src/views/dialogs/DepositDialog.tsx +++ b/src/views/dialogs/DepositDialog.tsx @@ -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) => { > {showTestDeposit ? : } - {/* TODO TRCL-1693 - uncomment when deposit form is ready + {/* TODO TRCL-1693 - uncomment when deposit form is ready */} {import.meta.env.MODE !== 'production' && ( setShowTestDeposit(!showTestDeposit)}> {showTestDeposit ? 'Show deposit form (Under Construction)' : 'Show test faucet'} - )} */} + )} ); diff --git a/src/views/dialogs/OnboardingDialog.tsx b/src/views/dialogs/OnboardingDialog.tsx index 66079c9..11593f3 100644 --- a/src/views/dialogs/OnboardingDialog.tsx +++ b/src/views/dialogs/OnboardingDialog.tsx @@ -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'; diff --git a/src/views/dialogs/WithdrawDialog.tsx b/src/views/dialogs/WithdrawDialog.tsx new file mode 100644 index 0000000..13ab110 --- /dev/null +++ b/src/views/dialogs/WithdrawDialog.tsx @@ -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 ( + + + + + + ); +}; + +const Styled: Record = {}; + +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; +`; diff --git a/src/views/forms/DepositForm/ChainSelectMenu.tsx b/src/views/forms/AccountManagementForms/ChainSelectMenu.tsx similarity index 50% rename from src/views/forms/DepositForm/ChainSelectMenu.tsx rename to src/views/forms/AccountManagementForms/ChainSelectMenu.tsx index 1d84f4c..94853de 100644 --- a/src/views/forms/DepositForm/ChainSelectMenu.tsx +++ b/src/views/forms/AccountManagementForms/ChainSelectMenu.tsx @@ -1,29 +1,43 @@ import type { ChainData } from '@0xsquid/sdk'; import styled, { type AnyStyledComponent } from 'styled-components'; +import { shallowEqual, useSelector } from 'react-redux'; +import { TransferInputChainResource, 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?: TransferInputChainResource; + onSelectChain: (chain: TransferInputChainResource) => 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: , + const chainItems = Object.values(chains).map((chain) => ({ + value: chain.type, + label: chain.stringKey, + onSelect: () => { + const selectedChain = resources?.chainResources?.get(chain.type); + selectedChain && onSelectChain(selectedChain); + }, + slotBefore: , })); return ( @@ -35,12 +49,12 @@ export const ChainSelectMenu = ({ chains = [], currentChain, setCurrentChain }: items: chainItems, }, ]} - label="Source" + label={label || (type === TransferType.deposit ? 'Source' : 'Destination')} > - {currentChain ? ( + {selectedChain ? ( <> - {currentChain?.chainName} + {selectedChain?.chainName} ) : ( 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); -`; +`; \ No newline at end of file diff --git a/src/views/forms/AccountManagementForms/DepositForm.tsx b/src/views/forms/AccountManagementForms/DepositForm.tsx new file mode 100644 index 0000000..0f6bb42 --- /dev/null +++ b/src/views/forms/AccountManagementForms/DepositForm.tsx @@ -0,0 +1,305 @@ +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 { + TransferInputField, + TransferInputChainResource, + TransferInputTokenResource, + TransferType, +} 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 { useDebounce, useStringGetter, useSquidRouter } 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 { Icon, IconName } from '@/components/Icon'; +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 abacusStateManager from '@/lib/abacus'; +import { MustBigNumber } from '@/lib/numbers'; + +import { ChainSelectMenu } from './ChainSelectMenu'; +import { TokenSelectMenu } from './TokenSelectMenu'; + +import { DepositButtonAndReceipt } from './DepositForm/DepositButtonAndReceipt'; + +import { getTransferInputs } from '@/state/inputsSelectors'; + +type DepositFormProps = { + onDeposit?: () => void; + onError?: () => void; +}; + +export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { + const stringGetter = useStringGetter(); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const { submitDeposit, axelarscanURL } = useSquidRouter(); + + const { requestPayload, token, chain, resources } = useSelector(getTransferInputs) || {}; + + // User inputs + const sourceChain = useMemo( + () => (chain ? resources?.chainResources?.get(chain) : undefined), + [chain, resources] + ); + + 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(fromAmount, 500); + + // Async Data + const [transactionHash, setTransactionHash] = useState(); + + const { balance, queryStatus, isQueryFetching } = useAccountBalance({ + addressOrDenom: sourceToken?.address || undefined, + assetSymbol: sourceToken?.symbol || undefined, + chainId: sourceChain?.chainId || undefined, + decimals: sourceToken?.decimals || undefined, + rpc: sourceChain?.rpc || 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({ + value: TransferType.deposit.rawValue, + field: TransferInputField.type, + }); + + return () => { + abacusStateManager.clearTransferInputValues(); + }; + }, []); + + useEffect(() => { + if (error) onError?.(); + }, [error]); + + const onSelectChain = useCallback((chain: TransferInputChainResource) => { + if (chain) { + abacusStateManager.clearTransferInputValues(); + abacusStateManager.setTransferValue({ + field: TransferInputField.chain, + value: chain.chainId, + }); + 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 amountInputReceipt = [ + { + key: 'amount', + label: ( + + {stringGetter({ key: STRING_KEYS.AVAILABLE })}{' '} + {sourceToken && {sourceToken.symbol}} + + ), + value: ( + + ), + }, + ]; + + 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 (!sourceChain) { + 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, sourceChain, fromAmount, sourceToken]) + + const isDisabled = + Boolean(errorMessage) || + !sourceToken || + !sourceChain || + debouncedAmountBN.isNaN() || + debouncedAmountBN.isZero(); + + if (!resources) { + return ; + } + + return ( + { + try { + e.preventDefault(); + setIsLoading(true); + const txHash = await submitDeposit(requestPayload); + onDeposit?.(); + setTransactionHash(txHash); + } catch (error) { + setError(error); + } finally { + setIsLoading(false); + } + }} + > + + + + + {stringGetter({ key: STRING_KEYS.MAX })} + + } + /> + + {errorMessage ? ( + {errorMessage} + ) : ( + transactionHash && ( + + + {stringGetter({ key: STRING_KEYS.DEPOSIT_IN_PROGRESS })} + + + + {stringGetter({ key: STRING_KEYS.VIEW_TRANSACTION })} + + + + + ) + )} + + + ); +}; + +const Styled: Record = {}; + +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.Link = styled(Link)` + color: var(--color-accent); + + &:visited { + color: var(--color-accent); + } +`; + +Styled.TransactionInfo = styled.span` + ${layoutMixins.row} +`; diff --git a/src/views/forms/DepositForm/DepositButtonAndReceipt.tsx b/src/views/forms/AccountManagementForms/DepositForm/DepositButtonAndReceipt.tsx similarity index 75% rename from src/views/forms/DepositForm/DepositButtonAndReceipt.tsx rename to src/views/forms/AccountManagementForms/DepositForm/DepositButtonAndReceipt.tsx index f7595a1..9939a8a 100644 --- a/src/views/forms/DepositForm/DepositButtonAndReceipt.tsx +++ b/src/views/forms/AccountManagementForms/DepositForm/DepositButtonAndReceipt.tsx @@ -1,4 +1,4 @@ -import { type Dispatch, type SetStateAction, useState } from 'react'; +import { type Dispatch, useMemo, type SetStateAction, useState, type ReactNode } from 'react'; import styled, { type AnyStyledComponent, css } from 'styled-components'; import { shallowEqual, useSelector } from 'react-redux'; import type { RouteData, TokenData } from '@0xsquid/sdk'; @@ -12,6 +12,7 @@ import { ButtonType, } from '@/constants/buttons'; +import { TransferInputTokenResource } from '@/constants/abacus'; import { STRING_KEYS } from '@/constants/localization'; import { NumberSign } from '@/constants/numbers'; @@ -30,11 +31,11 @@ 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 +44,9 @@ type ElementProps = { chainId?: string | number; setError?: Dispatch>; slippage: number; + slotError?: ReactNode; setSlippage: (slippage: number) => void; - sourceToken?: TokenData; + sourceToken?: TransferInputTokenResource; squidRoute?: RouteData; }; @@ -54,10 +56,9 @@ export const DepositButtonAndReceipt = ({ slippage, setSlippage, sourceToken, - squidRoute, isDisabled, - isLoading, + slotError, }: ElementProps) => { const [showFeeBreakdown, setShowFeeBreakdown] = useState(false); const [isEditingSlippage, setIsEditingSlipapge] = useState(false); @@ -78,35 +79,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: {name}, - value: , - })) - : []; - - if (gasCosts) { + if (typeof summary?.gasFee === 'number') { feeSubitems.push({ key: 'gas-fees', label: {stringGetter({ key: STRING_KEYS.GAS_FEE })}, - value: , + value: , + }); + } + + if (typeof summary?.bridgeFee === 'number') { + feeSubitems.push({ + key: 'bridge-fees', + label: Bridge Fee, + value: , }); } @@ -128,9 +117,9 @@ export const DepositButtonAndReceipt = ({ ), }, @@ -154,27 +143,20 @@ export const DepositButtonAndReceipt = ({ { key: 'exchange-rate', label: {stringGetter({ key: STRING_KEYS.EXCHANGE_RATE })}, - value: sourceToken && squidRoute && ( + value: typeof summary?.exchangeRate === 'number' && ( - {sourceToken?.symbol}} - /> + = - {squidRouter.SQUID_ROUTE_DEFAULTS.toToken.toUpperCase()}} - /> + ), }, { key: 'total-fees', label: {stringGetter({ key: STRING_KEYS.TOTAL_FEES })}, - value: squidRoute && , + value: typeof summary?.bridgeFee === 'number' && typeof summary?.gasFee === 'number' && ( + + ), subitems: feeSubitems, }, { @@ -191,6 +173,7 @@ export const DepositButtonAndReceipt = ({ ]; const isFormValid = !isDisabled && !isEditingSlippage; + const isLoading = isFormValid && !requestPayload; return ( } + slotError={slotError} > {!isMatchingNetwork ? ( ) : ( - )} @@ -234,7 +222,7 @@ const Styled: Record = {}; Styled.ExchangeRate = styled.span` ${layoutMixins.row} - gap: 1ch; + gap: 0.5ch; `; Styled.WithReceipt = styled(WithReceipt)` diff --git a/src/views/forms/DepositForm/SlippageEditor.tsx b/src/views/forms/AccountManagementForms/SlippageEditor.tsx similarity index 96% rename from src/views/forms/DepositForm/SlippageEditor.tsx rename to src/views/forms/AccountManagementForms/SlippageEditor.tsx index f830e83..85263c9 100644 --- a/src/views/forms/DepositForm/SlippageEditor.tsx +++ b/src/views/forms/AccountManagementForms/SlippageEditor.tsx @@ -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 ), [EditorState.Editing]: ( - 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: , + })); + + return ( + + {type === TransferType.deposit ? selectedToken?.symbol : 'USDC'} + + {type === TransferType.deposit ? 'USDC' : selectedToken?.symbol} + + ), + tooltip: 'swap', + }, + ]} + > + + {selectedToken ? ( + <> + {selectedToken?.name}{' '} + {selectedToken?.symbol} + + ) : ( + stringGetter({ key: STRING_KEYS.SELECT_ASSET }) + )} + + + ); +}; + +const Styled: Record = {}; + +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; +`; diff --git a/src/views/forms/AccountManagementForms/WithdrawForm.tsx b/src/views/forms/AccountManagementForms/WithdrawForm.tsx new file mode 100644 index 0000000..1588c3d --- /dev/null +++ b/src/views/forms/AccountManagementForms/WithdrawForm.tsx @@ -0,0 +1,309 @@ +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 type { TokenData, ChainData } from '@0xsquid/sdk'; +import { shallowEqual, useSelector } from 'react-redux'; +import { parseUnits } from 'viem'; + +import { TransferInputField, TransferInputChainResource, TransferInputTokenResource, TransferType } from '@/constants/abacus'; +import { AlertType } from '@/constants/alerts'; +import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; +import { STRING_KEYS } from '@/constants/localization'; +import { CLIENT_NETWORK_CONFIGS, type DydxV4Network } from '@/constants/networks'; +import { NumberSign } from '@/constants/numbers'; + +import { useAccounts, useDebounce, useStringGetter, useSquidRouter } from '@/hooks'; +import { SQUID_WITHDRAW_ROUTE_DEFAULTS } from '@/hooks/useSquidRouter'; + +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 { Icon, IconName } from '@/components/Icon'; +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 { dydxAddress } = useAccounts(); + const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {}; + + const { + error, + setError, + isLoading, + + axelarscanURL, + } = useSquidRouter({ allChains: true }); + + // User input + const [withdrawAmount, setWithdrawAmount] = useState(''); + const [slippage, setSlippage] = useState(0.001); // 0.1% slippage + const debouncedAmount = useDebounce(withdrawAmount, 500); + + const { requestPayload, token, chain, address: toAddress, resources } = useSelector(getTransferInputs) || {}; + + const toChain = useMemo( + () => (chain ? resources?.chainResources?.get(chain) : undefined), + [chain, resources] + ); + const toToken = useMemo( + () => (token ? resources?.tokenResources?.get(token) : undefined), + [token, resources] + ); + + // Async Data + const [transactionHash, setTransactionHash] = useState(); + + const debouncedAmountBN = MustBigNumber(debouncedAmount); + const withdrawAmountBN = MustBigNumber(withdrawAmount); + const freeCollateralBN = MustBigNumber(freeCollateral?.current); + + useEffect(() => { + abacusStateManager.setTransferValue({ + value: TransferType.withdrawal.rawValue, + field: TransferInputField.type, + }); + + return () => { + abacusStateManager.clearTransferInputValues(); + }; + }, []); + + useEffect(() => { + const hasInvalidInput = debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0); + + abacusStateManager.setTransferValue({ + value: hasInvalidInput ? 0 : debouncedAmount, + field: TransferInputField.usdcSize, + }); + + setError(null); + }, [debouncedAmountBN.toNumber()]); + + /** + * @todo Withdrawing involves two steps. + * 1. MsgCreateTransfer for withdrawing from the subaccount. + * 2. MsgTransfer to IBC transfer the funds to the destination chain. 0xSquid will provide the IBC path instructions. + */ + const onSubmit = useCallback( + async (e: FormEvent) => { + e.preventDefault(); + // const withdraw = await dydxClient.withdrawFromSubaccount(debouncedAmountBN.toNumber(), 0); + // const txHash = await executeRoute(); + // @ts-ignore execute route returns Promise instead of Promise + // setTransactionHash(txHash); + }, + [setTransactionHash] + ); + + const onChangeAddress = useCallback( + (e: ChangeEvent) => { + abacusStateManager.setTransferValue({ + field: TransferInputField.address, + value: e.target.value, + }); + }, + [] + ); + + const onChangeAmount = useCallback( + ({ value }: NumberFormatValues) => { + setWithdrawAmount(value); + }, + [setWithdrawAmount] + ); + + const onSetSlippage = useCallback( + (newSlippage: number) => { + setSlippage(newSlippage); + + // 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: TransferInputChainResource) => { + if (chain) { + abacusStateManager.clearTransferInputValues(); + abacusStateManager.setTransferValue({ + field: TransferInputField.chain, + value: chain.chainId, + }); + setWithdrawAmount(''); + } + }, []); + + const onSelectToken = useCallback( + (token: TransferInputTokenResource) => { + if (token) { + abacusStateManager.clearTransferInputValues(); + abacusStateManager.setTransferValue({ + field: TransferInputField.token, + value: token.address, + }); + setWithdrawAmount(''); + } + }, + [] + ); + + const amountInputReceipt = [ + { + key: 'freeCollateral', + label: ( + + {stringGetter({ key: STRING_KEYS.FREE_COLLATERAL })} USDC + + ), + value: ( + + ), + }, + ]; + + let errorMessage = !toAddress ? 'Please enter a destination address' : undefined; + + const isDisabled = !!errorMessage || + !toToken || + !toChain || + debouncedAmountBN.isNaN() || + debouncedAmountBN.isZero(); + + return ( + + + + + + + + + {stringGetter({ key: STRING_KEYS.MAX })} + + } + /> + + {errorMessage ? ( + {errorMessage} + ) : ( + transactionHash && ( + + + {stringGetter({ key: STRING_KEYS.DEPOSIT_IN_PROGRESS })} + + + + {stringGetter({ key: STRING_KEYS.VIEW_TRANSACTION })} + + + + + ) + )} + + + ); +}; + +const Styled: Record = {}; + +Styled.DiffOutput = styled(DiffOutput)` + --diffOutput-valueWithDiff-fontSize: 1em; +`; + +Styled.Form = styled.form` + 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} +`; \ No newline at end of file diff --git a/src/views/forms/AccountManagementForms/WithdrawForm/WithdrawButtonAndReceipt.tsx b/src/views/forms/AccountManagementForms/WithdrawForm/WithdrawButtonAndReceipt.tsx new file mode 100644 index 0000000..f393d80 --- /dev/null +++ b/src/views/forms/AccountManagementForms/WithdrawForm/WithdrawButtonAndReceipt.tsx @@ -0,0 +1,249 @@ +import { type Dispatch, type SetStateAction, useState } from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; +import styled, { type AnyStyledComponent } from 'styled-components'; +import { ChainType } from '@0xsquid/sdk'; +import type { ChainData, CosmosChain, RouteData, TokenData } from '@0xsquid/sdk'; +import BigNumber from 'bignumber.js'; +import { parseUnits } from 'viem'; + +import { TransferInputChainResource, 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 { Tag } from '@/components/Tag'; +import { ToggleButton } from '@/components/ToggleButton'; +import { WithReceipt } from '@/components/WithReceipt'; + +import { BIG_NUMBERS, MustBigNumber } from '@/lib/numbers'; + +import { SlippageEditor } from '../SlippageEditor'; + +import { getSubaccount } from '@/state/accountSelectors'; +import { getTransferInputs } from '@/state/inputsSelectors'; + +type ElementProps = { + setError?: Dispatch>; + setSlippage: (slippage: number) => void; + + slippage: number; + withdrawChain?: TransferInputChainResource; + withdrawToken?: TransferInputTokenResource; + + isDisabled?: boolean; + isLoading?: boolean; +}; + +export const WithdrawButtonAndReceipt = ({ + setError, + 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?.chainId || undefined, + decimals: withdrawToken?.decimals || undefined, + rpc: withdrawChain?.rpc || undefined, + isCosmosChain: false, + }); + + const balanceBN = MustBigNumber(balance); + // const newBalance = + // // toAmountMin && withdrawToken && parseUnits(toAmountMin, withdrawToken.decimals).toString(); + + const { leverage } = useSelector(getSubaccount, shallowEqual) || {}; + const { summary, requestPayload } = useSelector(getTransferInputs) || {}; + + const feeSubitems: DetailsItem[] = []; + + if (typeof summary?.gasFee === 'number') { + feeSubitems.push({ + key: 'gas-fees', + label: {stringGetter({ key: STRING_KEYS.GAS_FEE })}, + value: , + }); + } + + if (typeof summary?.bridgeFee === 'number') { + feeSubitems.push({ + key: 'bridge-fees', + label: Bridge Fee, + value: , + }); + } + + 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: {stringGetter({ key: STRING_KEYS.TOTAL_FEES })}, + value: typeof summary?.bridgeFee === 'number' && typeof summary?.gasFee === 'number' && ( + + ), + subitems: feeSubitems, + }, + { + key: 'leverage', + label: {stringGetter({ key: STRING_KEYS.LEVERAGE })}, + value: ( + + ), + }, + { + key: 'exchange-rate', + label: {stringGetter({ key: STRING_KEYS.EXCHANGE_RATE })}, + value: withdrawToken && ( + + + = + + + ), + }, + { + key: 'slippage', + label: {stringGetter({ key: STRING_KEYS.SLIPPAGE })}, + value: ( + + ), + }, + // { + // key: 'wallet', + // label: ( + // + // {stringGetter({ key: STRING_KEYS.WALLET })}{' '} + // {withdrawToken && {withdrawToken?.symbol}} + // + // ), + // value: ( + // + // ), + // }, + ]; + + return ( + + + {hasSubitems && ( + + setShowFeeBreakdown(!showFeeBreakdown)} + slotLeft={} + > + {showSubitemsToggle} + + + )} + + } + > + + + ); +}; + +const Styled: Record = {}; + +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); + } + } +`; diff --git a/src/views/forms/DepositForm.tsx b/src/views/forms/DepositForm.tsx deleted file mode 100644 index d64d522..0000000 --- a/src/views/forms/DepositForm.tsx +++ /dev/null @@ -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(defaultChain); - - const defaultToken: TokenData | undefined = useMemo( - () => tokens?.filter(({ chainId }) => chainId === currentChain?.chainId)?.[0] ?? undefined, - [currentChain] - ); - const [sourceToken, setSourceToken] = useState(defaultToken); - const [fromAmount, setFromAmount] = useState(''); - const [slippage, setSlippage] = useState(0.001); // 0.1% slippage - - // Async Data - const [squidRoute, setSquidRoute] = useState(); - const [transactionHash, setTransactionHash] = useState(); - - // Form states - const [error, setError] = useState(); - 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: ( - - {stringGetter({ key: STRING_KEYS.AVAILABLE })}{' '} - {sourceToken && {sourceToken.symbol}} - - ), - value: ( - - ), - }, - ]; - - 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 && ( - - {stringGetter({ key: STRING_KEYS.VIEW_DETAILS })} - {' on '} - - Axelar - - . - - )), - }; - - return ( - { - e.preventDefault(); - requestDeposit(); - }} - > - - { - setSourceToken(token); - - if (fromAmount) { - debouncedGetRoute({ - amount: fromAmount, - currentChain, - cosmosAddress, - fromAmount, - setError, - setIsLoading, - setSquidRoute, - slippage, - sourceToken: token, - }); - } else { - setSquidRoute(undefined); - } - }} - /> - - - - {stringGetter({ key: STRING_KEYS.MAX })} - - } - /> - - - {alertMessage.message && ( - {alertMessage.message} - )} - - - - - ); -}; - -const Styled: Record = {}; - -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; -`; diff --git a/src/views/forms/DepositForm/TokenSelectMenu.tsx b/src/views/forms/DepositForm/TokenSelectMenu.tsx deleted file mode 100644 index f93d2c5..0000000 --- a/src/views/forms/DepositForm/TokenSelectMenu.tsx +++ /dev/null @@ -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: , - })); - - return ( - - {sourceToken?.symbol}USDC - - ), - tooltip: 'swap', - withTooltipIcon: true, - }, - ]} - > - - {sourceToken ? ( - <> - {sourceToken?.name}{' '} - {sourceToken?.symbol} - - ) : ( - stringGetter({ key: STRING_KEYS.SELECT_ASSET }) - )} - - - ); -}; - -const Styled: Record = {}; - -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%; -`; diff --git a/src/views/forms/DepositForm/useSquidData.ts b/src/views/forms/DepositForm/useSquidData.ts deleted file mode 100644 index 166b3c7..0000000 --- a/src/views/forms/DepositForm/useSquidData.ts +++ /dev/null @@ -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, - }; -};