diff --git a/public/configs/v1/env.json b/public/configs/v1/env.json index 6542906d12..a2b582fe7f 100644 --- a/public/configs/v1/env.json +++ b/public/configs/v1/env.json @@ -1,7 +1,7 @@ { "apps": { "ios": { - "scheme": "dydx-t-v4" + "scheme": "dydxv4" } }, "tokens": { @@ -35,11 +35,11 @@ "image": "/currencies/usdc.png" } }, - "[mainnet chain id]": { - "comment": "Change according to mainnet release", + "dydx-mainnet-1": { + "comment": "Mainnet", "chain": { - "name": "TokenName", - "denom": "tokenDenom", + "name": "DYDX", + "denom": "adydx", "decimals": 18, "image": "/currencies/dydx.png" }, @@ -74,7 +74,7 @@ "accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain", "walletLearnMore": "https://www.dydx.academy/video/defi-wallet", "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665", - "adjustTargetLeverageLearnMore": "", + "adjustTargetLeverageLearnMore": "https://help.dydx.trade/en/articles/172975-isolated-margin", "launchIncentive": "https://cloud.chaoslabs.co", "tradingRewardsLearnMore": "https://docs.dydx.exchange/concepts-trading/rewards_fees_and_parameters", "exchangeStats": "https://app.mode.com/dydx_eng/reports/58822121650d?secret_key=391d9214fe6aefec35b7d35c", @@ -112,7 +112,7 @@ "keplrDashboard": "https://testnet.keplr.app/", "strideZoneApp": "https://testnet.stride.zone", "accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain", - "adjustTargetLeverageLearnMore": "", + "adjustTargetLeverageLearnMore": "https://help.dydx.trade/en/articles/172975-isolated-margin", "walletLearnMore": "https://www.dydx.academy/video/defi-wallet", "withdrawalGateLearnMore": "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665", "launchIncentive": "https://cloud.chaoslabs.co", @@ -132,45 +132,46 @@ "dydxLearnMore": "https://www.mintscan.io/dydx", "affiliateProgram": "" }, - "[mainnet chain id]": { - "tos": "[HTTP link to TOS]", - "privacy": "[HTTP link to Privacy Policy]", - "statusPage": "[HTTP link to status page]", - "mintscan": "[HTTP link to Mintscan, with {tx_hash} placeholder]", - "mintscanBase": "[HTTP link to TOS mintscan base url]", - "feedback": "[HTTP link to feedback form, can be null]", - "blogs": "[HTTP link to blogs, can be null]", - "foundation": "[HTTP link to foundation, can be null]", - "reduceOnlyLearnMore": "[HTTP link to reduce-only learn more, can be null]", - "documentation": "[HTTP link to documentation, can be null]", - "community": "[HTTP link to community, can be null]", - "help": "[HTTP link to help page, can be null]", - "vaultLearnMore": "[HTTP link to help page, can be null]", - "governanceLearnMore": "[HTTP link to governance learn more, can be null]", - "newMarketProposalLearnMore": "[HTTP link to new market proposal learn more, can be null]", - "stakingLearnMore": "[HTTP link to staking learn more, can be null]", - "keplrDashboard": "[HTTP link to keplr dashboard, can be null]", - "strideZoneApp": "[HTTP link to stride zone app, can be null]", - "accountExportLearnMore": "[HTTP link to account export learn more, can be null]", - "adjustTargetLeverageLearnMore": "[HTTP link to adjust target leverage learn more, can be null]", - "walletLearnMore": "[HTTP link to wallet learn more, can be null]", - "withdrawalGateLearnMore": "[HTTP link to withdrawal gate learn more, can be null]", - "launchIncentive": "[HTTP link to launch incentive host, can be null]", - "tradingRewardsLearnMore": "[HTTP link to trading rewards learn more, can be null]", - "exchangeStats": "[HTTP link to exchange stats, can be null]", - "initialMarginFractionLearnMore": "[HTTP link to governance functionalities liquidity tiers, can be null]", - "equityTiersLearnMore": "[HTTP link to equity tiers learn more, can be null]", - "contractLossMechanismLearnMore": "[HTTP link to documentation on contract loss mechanisms]", - "isolatedMarginLearnMore": "[HTTP link to documentation on isolated margin]", - "mintscanValidatorsLearnMore": "[HTTP link to mintscan info on validators]", - "protocolStaking": "[HTTP link to protocol staking info]", - "stakingAndClaimingRewardsLearnMore": "[HTTP link to staking and claiming rewards learn more]", - "predictionMarketLearnMore": "[HTTP link to prediction market learn more]", - "discoveryProgram": "[HTTP link to discovery program learn more]", - "getInTouch": "[HTTP link to get in touch with traders]", - "deployerTermsAndConditions": "[HTTP link to terms and conditions, can be null]", - "dydxLearnMore": "[HTTP link to information about the dYdX blockchain]", - "affiliateProgram": "[HTTP link to information about the affiliate program]" + "dydx-mainnet-1": { + "tos": "https://dydx.trade/terms", + "privacy": "https://dydx.trade/privacy", + "statusPage": "https://status.dydx.trade/", + "mintscan": "https://www.mintscan.io/dydx/tx/{tx_hash}", + "mintscanBase": "https://www.mintscan.io/dydx", + "feedback": "https://www.dydxopsdao.com/feedback", + "blogs": "https://www.dydx.foundation/blog", + "foundation": "https://www.dydx.foundation", + "reduceOnlyLearnMore": "https://help.dydx.trade/en/articles/8607918-reduce-only-order", + "documentation": "https://docs.dydx.exchange/", + "community": "https://discord.com/invite/dydx", + "help": "https://help.dydx.trade/", + "governanceLearnMore": "https://help.dydx.trade/en/collections/6936883-governance-staking", + "newMarketProposalLearnMore": "https://dydx.exchange/blog/new-market-proposals", + "stakingLearnMore": "https://help.dydx.trade/en/articles/8581388-accessing-governance-and-staking-on-dydx-chain", + "keplrDashboard": "https://wallet.keplr.app/chains/dydx?tab=staking", + "strideZoneApp": "https://app.stride.zone/?chain=DYDX", + "accountExportLearnMore": "https://help.dydx.trade/en/articles/8581269-secret-phrase-on-dydx-chain", + "adjustTargetLeverageLearnMore": "https://help.dydx.trade/en/articles/172975-isolated-margin", + "walletLearnMore": "https://help.dydx.trade/en/articles/166997-supported-default-wallets-on-dydx-chain", + "withdrawalGateLearnMore": "https://help.dydx.trade/en/articles/9004706-withdrawals-on-dydx-chain#h_e61f043370", + "launchIncentive": "https://cloud.chaoslabs.co", + "tradingRewardsLearnMore": "https://docs.dydx.exchange/concepts-trading/rewards_fees_and_parameters", + "exchangeStats": "https://app.mode.com/dydx_eng/reports/58822121650d?secret_key=391d9214fe6aefec35b7d35c", + "initialMarginFractionLearnMore": "https://docs.dydx.exchange/governance/functionalities#liquidity-tiers", + "equityTiersLearnMore": "https://help.dydx.trade/en/articles/171918-equity-tiers-and-rate-limits", + "fetAgixMarketWindDownProposal": "https://www.mintscan.io/dydx/proposals/61", + "contractLossMechanismLearnMore": "https://help.dydx.trade/en/articles/166973-contract-loss-mechanisms-on-dydx-chain", + "isolatedMarginLearnMore": "https://help.dydx.trade/en/articles/172975-isolated-margin", + "mintscanValidatorsLearnMore": "https://www.mintscan.io/dydx/validators", + "protocolStaking": "https://protocolstaking.info/", + "stakingAndClaimingRewardsLearnMore": "https://help.dydx.trade/en/articles/178571-staking-and-unstaking-dydx-and-claiming-staking-rewards", + "rndrParamProposal": "https://www.mintscan.io/dydx/proposals/126", + "predictionMarketLearnMore": "https://help.dydx.trade/en/articles/221756-prediction-markets-faq", + "discoveryProgram": "https://www.dydx.foundation/blog/dydx-discovery-user-interviews?utm_source=dYdXTelegram&utm_medium=GlobalSocial&utm_campaign=GlobalSocial", + "getInTouch": "https://t.me/+amt-yoIUwDplN2I5", + "deployerTermsAndConditions": "https://www.dydx.trade/terms", + "dydxLearnMore": "https://www.mintscan.io/dydx", + "affiliateProgram": "" } }, "wallets": { @@ -210,21 +211,21 @@ "signTypedDataAction": "dYdX Chain Onboarding", "signTypedDataDomainName": "dYdX Chain" }, - "[mainnet chain id]": { + "dydx-mainnet-1": { "walletconnect": { "client": { - "name": "[Name of the app]", - "description": "[Description of the app]", - "iconUrl": "[Relative URL of the icon URL]" + "name": "dYdX v4", + "description": "dYdX v4 App", + "iconUrl": "/logos/dydx-x.png" }, "v2": { - "projectId": "[Project ID]" + "projectId": "fd67e5fbec90c07b6012699738d4a487" } }, "walletSegue": { - "callbackUrl": "[Relative callback URL for WalletSegue, should match apple-app-site-association]" + "callbackUrl": "/walletsegue" }, - "images": "[Relative URL for wallet images]", + "images": "/wallets/", "signTypedDataAction": "dYdX Chain Onboarding", "signTypedDataDomainName": "dYdX Chain" } @@ -244,11 +245,11 @@ "newMarketsMethodology": "https://docs.google.com/spreadsheets/d/1zjkV9R7R_7KMItuzqzvKGwefSBRfE-ZNAx1LH55OcqY/edit?usp=sharing" } }, - "[mainnet chain id]": { + "dydx-mainnet-1": { "newMarketProposal": { - "initialDepositAmount": 0, - "delayBlocks": 0, - "newMarketsMethodology": "[URL to spreadsheet or document that explains methodology]" + "initialDepositAmount": 2000000000000000000000, + "delayBlocks": 3600, + "newMarketsMethodology": "https://docs.google.com/spreadsheets/d/1046uSR2sltA6siZGnvBlgUp4xlPNM7dPFvUgdACgTy4/edit?usp=sharing" } } }, @@ -555,7 +556,7 @@ "ios": { "minimalVersion": "1.0", "build": 40000, - "url": "https://apps.apple.com/app/dydx/id1564787350" + "url": "https://apps.apple.com/app/dydx/id6475599596" }, "android": { "minimalVersion": "1.1", @@ -612,7 +613,7 @@ } }, "dydxprotocol-testnet": { - "name": "v4 Public Testnet", + "name": "Testnet", "ethereumChainId": "11155111", "dydxChainId": "dydx-testnet-4", "chainName": "dYdX Chain", @@ -892,40 +893,77 @@ } }, "dydxprotocol-mainnet": { - "name": "v4", + "name": "Mainnet", "ethereumChainId": "1", - "dydxChainId": "[mainnet chain id]", + "dydxChainId": "dydx-mainnet-1", "chainName": "dYdX Chain", "chainLogo": "/dydx-chain.png", - "deployerName": "[deployer name]", + "deployerName": "dYdX Ops subDAO", "rewardsHistoryStartDateMs": "1706486400000", "megavaultHistoryStartDateMs": "1704844800000", "isMainNet": true, "endpoints": { "indexers": [ { - "api": "[REST endpoint]", - "socket": "[Websocket endpoint]" + "api": "https://indexer.dydx.trade", + "socket": "wss://indexer.dydx.trade" } ], "validators": [ - "[Validator endpoint 1", - "[Validator endpoint n]" - ], - "skip": "[Skip endpoint for mainnet]", - "solanaRpcUrl": "[Solana rpc url for mainnet]", - "nobleValidator": "[noble validator endpoint for mainnet]", - "osmosisValidator": "[osmosis validator endpoint for mainnet]", - "neutronValidator": "[neutron validator endpoint for mainnet]", - "geo": "[geo endpoint for mainnet]", - "stakingAPR": "[staking APR endpoint for mainnet]" + "https://dydx-dao-rpc.polkachu.com", + "https://dydx-mainnet-full-rpc.public.blastapi.io", + "https://dydx-ops-rpc.kingnodes.com" + ], + "0xsquid": "https://api.squidrouter.com", + "skip": "https://skip-proxy-web-mainnet.infrastructure-34d.workers.dev", + "nobleValidator": "https://noble-yx-rpc.polkachu.com/", + "geo": "https://api.dydx.exchange/v4/geo", + "stakingAPR": "https://apybara-proxy-web-mainnet.infrastructure-34d.workers.dev/v0/protocols/dydx", + "solanaRpcUrl": "https://alchemy-solana-proxy-web-mainnet.infrastructure-34d.workers.dev", + "osmosisValidator": "https://rpc.osmosis.zone", + "neutronValidator": "https://neutron-rpc.publicnode.com" }, - "stakingValidators": [], + "ios": { + "minimalVersion": "1.8.0", + "build": 31072, + "url": "https://apps.apple.com/app/id6475599596" + }, + "android": { + "minimalVersion": "1.9.0", + "build": 54, + "url": "https://play.google.com/store/apps/details?id=trade.opsdao.dydxchain&pli=1" + }, + "apps": { + "ios": { + "minimalVersion": "1.7.0", + "build": 31072, + "url": "https://apps.apple.com/app/id6475599596" + }, + "android": { + "minimalVersion": "1.9.0", + "build": 54, + "url": "https://play.google.com/store/apps/details?id=trade.opsdao.dydxchain&pli=1" + } + }, + "stakingValidators": [ + "dydxvaloper15xgxv2j45uc4er8z9tfz5m0f0e74ymv6xj9l9d", + "dydxvaloper1cpz0jtj6rkezzs7z8k5gy6py05sxdkmyvggvvh", + "dydxvaloper199fjq4rnfvz24cktl8cervx8h8e90ruk3yrrdn", + "dydxvaloper1mwhwf9rqh64ktr8t8xnz37nhg7vvy42ec56jwe", + "dydxvaloper140l6y2gp3gxvay6qtn70re7z2s0gn57zq7tkd8", + "dydxvaloper1y6ncfxx8x9sqec97pehjw0k32slw63850ltjrn", + "dydxvaloper15wphegl8esn7r2rgj9j3xf870v78lxg8yfjn95", + "dydxvaloper1t8hjjca8kecjtuhs2qy83maurj78fq2z5c44z5", + "dydxvaloper1j4ljutnh66r55a29jydgca7pfmhd40e3nlcfa4" + ], "featureFlags": { - "checkForGeo": false, + "checkForGeo": true, + "reduceOnlySupported": true, + "usePessimisticCollateralCheck": false, + "useOptimisticCollateralCheck": true, "withdrawalSafetyEnabled": true, "CCTPWithdrawalOnly": true, - "CCTPDepositOnly": true, + "CCTPDepositOnly": false, "isSlTpEnabled": true, "isSlTpLimitOrdersEnabled": false } diff --git a/src/views/forms/AccountManagementForms/SlippageEditor.tsx b/src/views/forms/AccountManagementForms/SlippageEditor.tsx index 89faad2749..4c77395761 100644 --- a/src/views/forms/AccountManagementForms/SlippageEditor.tsx +++ b/src/views/forms/AccountManagementForms/SlippageEditor.tsx @@ -49,12 +49,11 @@ export const SlippageEditor = ({ setIsEditing?.(editorState !== EditorState.Viewing); if (editorState === EditorState.Selecting) { - // use setTimeout with a 0ms delay to focus asynchronously. - setTimeout(() => toggleGroupRef?.current?.focus(), 0); + toggleGroupRef?.current?.focus(); } else if (editorState === EditorState.Editing) { inputRef?.current?.focus(); } - }, [editorState]); + }, [editorState, setIsEditing]); const onOpenChange = (isOpen: boolean) => { if (!isOpen) { diff --git a/src/views/forms/AccountManagementFormsNew/WithdrawForm/WithdrawForm.tsx b/src/views/forms/AccountManagementFormsNew/WithdrawForm/WithdrawForm.tsx new file mode 100644 index 0000000000..da960ab0c1 --- /dev/null +++ b/src/views/forms/AccountManagementFormsNew/WithdrawForm/WithdrawForm.tsx @@ -0,0 +1,536 @@ +import type { ChangeEvent, FormEvent } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { TYPE_URL_MSG_WITHDRAW_FROM_SUBACCOUNT } from '@dydxprotocol/v4-client-js'; +import { Asset } from '@skip-go/client'; +import { parseUnits } from 'ethers'; +import type { NumberFormatValues } from 'react-number-format'; +import { shallowEqual } from 'react-redux'; +import styled from 'styled-components'; + +import { AutoSweepConfig } from '@/constants/abacus'; +import { AlertType } from '@/constants/alerts'; +import { AnalyticsEvents } from '@/constants/analytics'; +import { ButtonSize } from '@/constants/buttons'; +import { isTokenCctp } from '@/constants/cctp'; +import { getNobleChainId, getSolanaChainId, GRAZ_CHAINS } from '@/constants/graz'; +import { STRING_KEYS } from '@/constants/localization'; +import { TransferNotificationTypes } from '@/constants/notifications'; +import { NumberSign, USD_DECIMALS } from '@/constants/numbers'; +import { TransferType } from '@/constants/transfers'; +import { WalletType } from '@/constants/wallets'; + +import { useSkipClient } from '@/hooks/transfers/skipClient'; +import { useTransfers } from '@/hooks/transfers/useTransfers'; +import { useAccounts } from '@/hooks/useAccounts'; +import { useDydxClient } from '@/hooks/useDydxClient'; +import { useLocalNotifications } from '@/hooks/useLocalNotifications'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; + +import { formMixins } from '@/styles/formMixins'; + +import { AlertMessage } from '@/components/AlertMessage'; +import { DiffOutput } from '@/components/DiffOutput'; +import { FormInput } from '@/components/FormInput'; +import { FormMaxInputToggleButton } from '@/components/FormMaxInputToggleButton'; +import { Icon, IconName } from '@/components/Icon'; +import { InputType } from '@/components/Input'; +import { OutputType } from '@/components/Output'; +import { Tag } from '@/components/Tag'; +import { WithDetailsReceipt } from '@/components/WithDetailsReceipt'; +import { WithTooltip } from '@/components/WithTooltip'; + +import { getSubaccount } from '@/state/accountSelectors'; +import { getSelectedDydxChainId } from '@/state/appSelectors'; +import { useAppSelector } from '@/state/appTypes'; +import { getTransferInputs } from '@/state/inputsSelectors'; + +import { isValidAddress } from '@/lib/addressUtils'; +import { track } from '@/lib/analytics/analytics'; +import { dd } from '@/lib/analytics/datadog'; +import { MustBigNumber } from '@/lib/numbers'; +import { log } from '@/lib/telemetry'; + +import { NetworkSelectMenu } from './NetworkSelectMenu'; +import { TokenSelectMenu } from './TokenSelectMenu'; +import { useValidation } from './useValidation'; + +const DUMMY_TX_HASH = 'withdraw_dummy_tx_hash'; + +export const WithdrawForm = () => { + const stringGetter = useStringGetter(); + const [error, setError] = useState(); + const [isLoading, setIsLoading] = useState(false); + const selectedDydxChainId = useAppSelector(getSelectedDydxChainId); + + const { dydxAddress, sourceAccount, localDydxWallet, localNobleWallet } = useAccounts(); + const { freeCollateral } = useAppSelector(getSubaccount, shallowEqual) ?? {}; + + // TODO: https://linear.app/dydx/issue/OTE-867/coinbase-withdrawals + const { exchange } = useAppSelector(getTransferInputs, shallowEqual) ?? {}; + + // User input + const { usdcDenom, usdcDecimals } = useTokenConfigs(); + + const { + setFromTokenDenom, + defaultTokenDenom, + setToTokenDenom, + defaultChainId, + fromChainId, + setFromChainId, + toChainId, + setToChainId, + toAddress, + setToAddress, + setFromAddress, + debouncedAmount, + debouncedAmountBN, + setAmount, + setTransferType, + route, + txs, + toToken, + assetsForSelectedChain, + chainsForNetwork, + } = useTransfers(); + const { skipClient } = useSkipClient(); + + const isCctp = isTokenCctp(toToken); + const [slippage, setSlippage] = useState(isCctp ? 0 : 0.01); // 0.1% slippage + const isValidDestinationAddress = useMemo(() => { + const grazChainPrefix = + GRAZ_CHAINS.find((chain) => chain.chainId === toChainId)?.bech32Config.bech32PrefixAccAddr ?? + ''; + const prefix = exchange ? 'noble' : grazChainPrefix; + return isValidAddress({ + address: toAddress, + network: toChainId === getSolanaChainId() ? 'solana' : prefix ? 'cosmos' : 'evm', + prefix, + }); + }, [exchange, toAddress, toChainId]); + + const { addOrUpdateTransferNotification } = useLocalNotifications(); + + const freeCollateralBN = useMemo(() => MustBigNumber(freeCollateral?.current), [freeCollateral]); + + // Set default values for withdraw from + // TODO: https://linear.app/dydx/issue/OTE-875/calculate-default-withdrawal-address-for-keplr + // if wallet type is cosmos (keplr), change toAddress based on the chainid + // B/C cosmos handles multiple chains and each have their own address + useEffect(() => { + setTransferType(TransferType.Withdraw); + setFromChainId(selectedDydxChainId); + setFromAddress(dydxAddress); + setFromTokenDenom(usdcDenom); + setToAddress(sourceAccount.address); + }, [ + setTransferType, + setFromChainId, + selectedDydxChainId, + setFromAddress, + dydxAddress, + setFromTokenDenom, + usdcDenom, + setToAddress, + sourceAccount.address, + ]); + + useEffect(() => { + setToChainId(defaultChainId); + }, [defaultChainId, setToChainId]); + + useEffect(() => { + setToTokenDenom(defaultTokenDenom); + }, [defaultTokenDenom, setToTokenDenom]); + + const { screenAddresses } = useDydxClient(); + const nobleChainId = getNobleChainId(); + + const onSubmitComplete = useCallback( + (txHash: string | undefined, notificationId: string) => { + if (!txHash || !fromChainId || !toChainId) { + throw new Error('No transaction hash returned'); + } + setAmount(''); + + const notificationParams = { + id: notificationId, + txHash, + type: TransferNotificationTypes.Withdrawal, + toChainId, + fromChainId, + toAmount: Number(debouncedAmount), + triggeredAt: Date.now(), + isCctp, + isExchange: Boolean(exchange), + requestId: undefined, + }; + addOrUpdateTransferNotification({ ...notificationParams, txHash, isDummy: false }); + const transferWithdrawContext = { + chainId: toChainId, + tokenAddress: toToken?.denom ?? undefined, + tokenSymbol: toToken?.symbol ?? undefined, + slippage: slippage ?? undefined, + gasFee: undefined, + bridgeFee: Number(route?.usdAmountIn) - Number(route?.usdAmountOut), + exchangeRate: undefined, + estimatedRouteDuration: route?.estimatedRouteDurationSeconds ?? undefined, + toAmount: Number(route?.amountOut) ?? undefined, + toAmountMin: Number(route?.estimatedAmountOut) ?? undefined, + txHash, + }; + track(AnalyticsEvents.TransferWithdraw(transferWithdrawContext)); + dd.info('Transfer withdraw submitted', transferWithdrawContext); + }, + [ + addOrUpdateTransferNotification, + debouncedAmount, + exchange, + fromChainId, + isCctp, + route, + setAmount, + slippage, + toChainId, + toToken, + ] + ); + const submitCCTPWithdrawal = useCallback( + async (notificationId: string) => { + if (!route || !dydxAddress || !toAddress || !toChainId || !localNobleWallet?.address) return; + AutoSweepConfig.disable_autosweep = true; + await skipClient.executeRoute({ + route, + getCosmosSigner: async (chainID) => { + if (chainID === getNobleChainId()) { + if (!localNobleWallet?.offlineSigner) { + throw new Error('No local noblewallet offline signer. Cannot submit tx'); + } + return localNobleWallet?.offlineSigner; + } + if (!localDydxWallet?.offlineSigner) + throw new Error('No local dydxwallet offline signer. Cannot submit tx'); + return localDydxWallet?.offlineSigner; + }, + beforeMsg: { + msg: JSON.stringify({ + sender: { + owner: dydxAddress, + number: 0, + }, + recipient: dydxAddress, + assetId: 0, + quantums: parseUnits(debouncedAmount, usdcDecimals), + }), + msgTypeURL: TYPE_URL_MSG_WITHDRAW_FROM_SUBACCOUNT, + }, + // TODO: think about building this dynamically + // Right now we don't need to because every withdrawal follows the same cctp route + // dydx -> noble -> final destination + userAddresses: [ + { chainID: selectedDydxChainId, address: dydxAddress }, + { + chainID: getNobleChainId(), + address: localNobleWallet?.address, + }, + { + chainID: toChainId, + address: toAddress, + }, + ], + onTransactionBroadcast: async ({ txHash, chainID }) => { + // TODO: enable transfer notifications. This does not work yet + if (chainID === toChainId) onSubmitComplete(txHash, notificationId); + }, + onTransactionCompleted: async (chainID) => { + // once the transaction in noble is complete, we can be confident that + // there are no more funds in the noble wallet that need to be transferred + if (chainID === getNobleChainId()) { + AutoSweepConfig.disable_autosweep = false; + } + }, + }); + }, + [ + route, + dydxAddress, + toAddress, + toChainId, + localNobleWallet?.address, + localNobleWallet?.offlineSigner, + skipClient, + debouncedAmount, + usdcDecimals, + selectedDydxChainId, + localDydxWallet?.offlineSigner, + onSubmitComplete, + ] + ); + + const onSubmit = useCallback( + async (e: FormEvent) => { + const notificationId = crypto?.randomUUID() ?? Date.now().toString(); + + try { + e.preventDefault(); + + if (!txs || !debouncedAmount || !toAddress || !dydxAddress) { + throw new Error('Invalid request payload'); + } + + setIsLoading(true); + setError(undefined); + + const screenResults = await screenAddresses({ + addresses: [toAddress, dydxAddress], + }); + + if (screenResults?.[dydxAddress]) { + setError( + stringGetter({ + key: STRING_KEYS.WALLET_RESTRICTED_WITHDRAWAL_TRANSFER_ORIGINATION_ERROR_MESSAGE, + }) + ); + } else if (screenResults?.[toAddress]) { + setError( + stringGetter({ + key: STRING_KEYS.WALLET_RESTRICTED_WITHDRAWAL_TRANSFER_DESTINATION_ERROR_MESSAGE, + }) + ); + } else { + if (!isCctp) { + throw new Error('Only cctp routes are eligible for withdrawal'); + } + await submitCCTPWithdrawal(notificationId); + } + } catch (err) { + log('WithdrawForm/onSubmit', err); + if (err?.code === 429) { + setError(stringGetter({ key: STRING_KEYS.RATE_LIMIT_REACHED_ERROR_MESSAGE })); + } else { + setError( + err.message + ? stringGetter({ + key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE, + params: { + ERROR_MESSAGE: err.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }), + }, + }) + : stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG }) + ); + } + // if error update dummy notification with error + addOrUpdateTransferNotification({ + id: notificationId, + txHash: DUMMY_TX_HASH, + status: { error: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG }) }, + }); + } finally { + setIsLoading(false); + } + }, + [ + txs, + debouncedAmount, + toAddress, + dydxAddress, + screenAddresses, + stringGetter, + isCctp, + submitCCTPWithdrawal, + addOrUpdateTransferNotification, + ] + ); + + const onChangeAddress = useCallback( + (e: ChangeEvent) => { + setToAddress(e.target.value); + }, + [setToAddress] + ); + + const onChangeAmount = useCallback( + ({ value }: NumberFormatValues) => { + setAmount(value); + setError(undefined); + }, + [setAmount] + ); + + const onSetSlippage = useCallback( + (newSlippage: number) => { + setSlippage(newSlippage); + }, + [setSlippage] + ); + + const onClickMax = useCallback(() => { + setAmount(freeCollateralBN.toString()); + }, [freeCollateralBN, setAmount]); + + useEffect(() => { + if (sourceAccount?.walletInfo?.name === WalletType.Privy) { + // TODO: https://linear.app/dydx/issue/OTE-867/coinbase-withdrawals + // abacusStateManager.setTransferValue({ + // field: TransferInputField.exchange, + // value: 'coinbase', + // }); + } + }, [sourceAccount, nobleChainId, setToChainId]); + + const onSelectNetwork = useCallback( + (chainID: string, type: 'chain' | 'exchange') => { + if (chainID) { + setAmount(''); + if (type === 'chain') { + setToChainId(chainID); + } + } + }, + [setAmount, setToChainId] + ); + + const onSelectToken = useCallback((asset: Asset) => { + if (asset) { + setToTokenDenom(asset.denom); + setAmount(''); + } + }, []); + + const amountInputReceipt = [ + { + key: 'freeCollateral', + label: ( + + {stringGetter({ key: STRING_KEYS.FREE_COLLATERAL })} USDC + + ), + value: ( + + ), + }, + ]; + + const { errorMessage, alertType } = useValidation({ + isCctp, + debouncedAmountBN, + toAddress, + isValidDestinationAddress, + error, + toChainId, + toToken, + freeCollateralBN, + }); + + const isDisabled = + !!errorMessage || + !toToken || + (!toChainId && !exchange) || + debouncedAmountBN.isNaN() || + debouncedAmountBN.isZero() || + isLoading || + !isValidDestinationAddress; + + return ( + <$Form onSubmit={onSubmit}> +
+ {stringGetter({ + key: STRING_KEYS.LOWEST_FEE_WITHDRAWALS_SKIP, + params: { + LOWEST_FEE_TOKENS_TOOLTIP: ( + + {stringGetter({ + key: STRING_KEYS.SELECT_CHAINS, + })} + + ), + }, + })} +
+ + + {stringGetter({ key: STRING_KEYS.DESTINATION })}{' '} + {isValidDestinationAddress ? ( + + ) : null} + + } + validationConfig={ + toAddress && Boolean(exchange) && !isValidDestinationAddress + ? { + type: AlertType.Error, + message: stringGetter({ key: STRING_KEYS.NOBLE_ADDRESS_VALIDATION }), + } + : undefined + } + /> + + + (isPressed ? onClickMax() : setAmount(''))} + /> + } + /> + + {errorMessage && ( + + {errorMessage} + + )} + <$Footer>{/* TODO [onboarding-rewrite]: add preview */} + + ); +}; +const $Form = styled.form` + ${formMixins.transfersForm} +`; + +const $Footer = styled.footer` + ${formMixins.footer} + --stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom); +`;