diff --git a/package.json b/package.json index ab40d6e..b7b1aa9 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,8 @@ "@cosmjs/proto-signing": "^0.31.0", "@cosmjs/stargate": "^0.31.0", "@cosmjs/tendermint-rpc": "^0.31.0", - "@dydxprotocol/v4-abacus": "^1.0.30", - "@dydxprotocol/v4-client-js": "^1.0.0", + "@dydxprotocol/v4-abacus": "^1.1.4", + "@dydxprotocol/v4-client-js": "^1.0.6", "@dydxprotocol/v4-localization": "^1.0.17", "@ethersproject/providers": "^5.7.2", "@js-joda/core": "^5.5.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cfe48b7..bdb47df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: '@0xsquid/sdk': specifier: ^1.10.0 @@ -23,11 +27,11 @@ dependencies: specifier: ^0.31.0 version: 0.31.0 '@dydxprotocol/v4-abacus': - specifier: ^1.0.30 - version: 1.0.30 + specifier: ^1.1.4 + version: 1.1.4 '@dydxprotocol/v4-client-js': - specifier: ^1.0.0 - version: 1.0.0 + specifier: ^1.0.6 + version: 1.0.6 '@dydxprotocol/v4-localization': specifier: ^1.0.17 version: 1.0.17 @@ -984,12 +988,12 @@ packages: resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==} dev: true - /@dydxprotocol/v4-abacus@1.0.30: - resolution: {integrity: sha512-zzJzNzMR3Stwv1Mu0XGUNlyo1V/ywo3yi3QSzE4mzUtXB+p5Shj5hRQCcwhHcNjLgp6a+T0BcRjBPa4QMETmwg==} + /@dydxprotocol/v4-abacus@1.1.4: + resolution: {integrity: sha512-gml6qheFsPShE9p3FmzFeStYM9ZVhgMq0K0+y12V7pObnMKbumkOIISgCzc47idkah0GMNhiqwi9faD5SjOy/A==} dev: false - /@dydxprotocol/v4-client-js@1.0.0: - resolution: {integrity: sha512-ehfHO+zQy795TcJRwtiawadFHZyh4HnpJNP26hCGsIjLE5q6LLHweQHpK/1N/sXU1PBOuQwc7iaJnFop6gYauQ==} + /@dydxprotocol/v4-client-js@1.0.6: + resolution: {integrity: sha512-xiWH+kbix+zhI6EsAnd+NDvkjBgxWtGwQmvpd0PjljWNYSFgUtNe5M+piDdRbl2nhy6YWbxAGTwwS3K/ih5qSw==} dependencies: '@cosmjs/amino': 0.30.1 '@cosmjs/encoding': 0.31.1 @@ -14136,7 +14140,3 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: true - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/public/configs/cctp.json b/public/configs/cctp.json index 026ef11..61bd08a 100644 --- a/public/configs/cctp.json +++ b/public/configs/cctp.json @@ -1,7 +1,7 @@ [ { "chainId": "42161", - "tokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "tokenAddress": "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8", "name": "Arbitrum" }, { @@ -11,7 +11,7 @@ }, { "chainId": "8453", - "tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "tokenAddress": "0x66627F389ae46D881773B7131139b2411980E09E", "name": "Base" }, { @@ -21,7 +21,7 @@ }, { "chainId": "10", - "tokenAddress": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "tokenAddress": "0x7F5c764cBc14f9669B88837ca1490cCa17c31607", "name": "OP Mainnet" }, { diff --git a/src/App.tsx b/src/App.tsx index d9cf2a1..69ba18b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,7 +22,6 @@ import { NotificationsProvider } from '@/hooks/useNotifications'; import { LocalNotificationsProvider } from '@/hooks/useLocalNotifications'; import { RestrictionProvider } from '@/hooks/useRestrictions'; import { SubaccountProvider } from '@/hooks/useSubaccount'; -import { SquidProvider } from '@/hooks/useSquid'; import { TestFlagsProvider } from '@/hooks/useTestFlags'; import { GuardedMobileRoute } from '@/components/GuardedMobileRoute'; @@ -130,7 +129,6 @@ const providers = [ wrapProvider(DydxProvider), wrapProvider(AccountsProvider), wrapProvider(SubaccountProvider), - wrapProvider(SquidProvider), wrapProvider(LocalNotificationsProvider), wrapProvider(NotificationsProvider), wrapProvider(DialogAreaProvider), diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 855af88..dff6cdb 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -61,7 +61,7 @@ export const Button = forwardRef { diff --git a/src/constants/notifications.ts b/src/constants/notifications.ts index 2bae6d5..5262c83 100644 --- a/src/constants/notifications.ts +++ b/src/constants/notifications.ts @@ -129,6 +129,7 @@ export type TransferNotifcation = { fromChainId?: string; toAmount?: number; triggeredAt?: number; + isCctp?: boolean; errorCount?: number; status?: StatusResponse; }; diff --git a/src/hooks/useAccounts.tsx b/src/hooks/useAccounts.tsx index 96cd7bb..4bf529b 100644 --- a/src/hooks/useAccounts.tsx +++ b/src/hooks/useAccounts.tsx @@ -2,7 +2,7 @@ import { useCallback, useContext, createContext, useEffect, useState, useMemo } import { useDispatch } from 'react-redux'; import { AES, enc } from 'crypto-js'; -import { LocalWallet, type Subaccount } from '@dydxprotocol/v4-client-js'; +import { NOBLE_BECH32_PREFIX, LocalWallet, type Subaccount } from '@dydxprotocol/v4-client-js'; import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account'; import { DialogTypes } from '@/constants/dialogs'; @@ -219,6 +219,16 @@ const useAccountsContext = () => { else abacusStateManager.attemptDisconnectAccount(); }, [localDydxWallet]); + useEffect(() => { + const setNobleWallet = async () => { + if (hdKey?.mnemonic) { + const nobleWallet = await LocalWallet.fromMnemonic(hdKey.mnemonic, NOBLE_BECH32_PREFIX); + abacusStateManager.setNobleWallet(nobleWallet); + } + }; + setNobleWallet(); + }, [hdKey?.mnemonic]); + // clear subaccounts when no dydxAddress is set useEffect(() => { (async () => { diff --git a/src/hooks/useLocalNotifications.tsx b/src/hooks/useLocalNotifications.tsx index ac7c6a9..5b2742a 100644 --- a/src/hooks/useLocalNotifications.tsx +++ b/src/hooks/useLocalNotifications.tsx @@ -1,11 +1,11 @@ import { createContext, useContext, useCallback, useEffect, useMemo } from 'react'; import { useQuery } from 'react-query'; -import type { StatusResponse } from '@0xsquid/sdk'; import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage'; import { type TransferNotifcation } from '@/constants/notifications'; import { useAccounts } from '@/hooks/useAccounts'; -import { STATUS_ERROR_GRACE_PERIOD, useSquid } from '@/hooks/useSquid'; + +import { fetchSquidStatus, STATUS_ERROR_GRACE_PERIOD } from '@/lib/squid'; import { useLocalStorage } from './useLocalStorage'; @@ -68,8 +68,6 @@ const useLocalNotificationsContext = () => { [transferNotifications] ); - const squid = useSquid(); - useQuery({ queryKey: 'getTransactionStatus', queryFn: async () => { @@ -81,23 +79,27 @@ const useLocalNotificationsContext = () => { toChainId, fromChainId, triggeredAt, + isCctp, errorCount, status: currentStatus, } = transferNotification; + // @ts-ignore status.errors is not in the type definition but can be returned + // also error can some time come back as an empty object so we need to ignore for that + const hasErrors = !!currentStatus?.errors || + (currentStatus?.error && Object.keys(currentStatus.error).length !== 0); + if ( - // @ts-ignore status.errors is not in the type definition but can be returned - !currentStatus?.errors && - !currentStatus?.error && + !hasErrors && (!currentStatus?.squidTransactionStatus || currentStatus?.squidTransactionStatus === 'ongoing') ) { try { - const status = await squid?.getStatus({ + const status = await fetchSquidStatus({ transactionId: txHash, toChainId, fromChainId, - }); + }, isCctp); if (status) { transferNotification.status = status; diff --git a/src/hooks/useSquid.tsx b/src/hooks/useSquid.tsx deleted file mode 100644 index 1e3b240..0000000 --- a/src/hooks/useSquid.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { createContext, useContext, useEffect, useMemo, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { Squid } from '@0xsquid/sdk'; - -import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; - -import { getSelectedNetwork } from '@/state/appSelectors'; - -export const NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; - -export const STATUS_ERROR_GRACE_PERIOD = 300_000; - -const useSquidContext = () => { - const selectedNetwork = useSelector(getSelectedNetwork); - const [_, setInitialized] = useState(false); - - const initializeClient = async () => { - setInitialized(false); - if (!squid) return; - await squid.init(); - setInitialized(true); - }; - - const squid = useMemo( - () => - new Squid({ - baseUrl: ENVIRONMENT_CONFIG_MAP[selectedNetwork]?.endpoints['0xsquid'], - integratorId: ENVIRONMENT_CONFIG_MAP[selectedNetwork]?.squidIntegratorId, - }), - [selectedNetwork] - ); - - useEffect(() => { - if (squid) { - initializeClient(); - } - }, [squid]); - - return squid; -}; - -type SquidContextType = ReturnType; -const SquidContext = createContext(undefined); -SquidContext.displayName = '0xSquid'; - -export const SquidProvider = ({ ...props }) => ( - -); - -export const useSquid = () => useContext(SquidContext); diff --git a/src/lib/abacus/dydxChainTransactions.ts b/src/lib/abacus/dydxChainTransactions.ts index 9940389..70293ec 100644 --- a/src/lib/abacus/dydxChainTransactions.ts +++ b/src/lib/abacus/dydxChainTransactions.ts @@ -1,7 +1,7 @@ import Abacus, { type Nullable } from '@dydxprotocol/v4-abacus'; import Long from 'long'; import type { IndexedTx } from '@cosmjs/stargate'; -import { encodeJson } from '@dydxprotocol/v4-client-js'; +import { GAS_MULTIPLIER, encodeJson } from '@dydxprotocol/v4-client-js'; import { CompositeClient, @@ -9,6 +9,7 @@ import { type LocalWallet, Network, NetworkOptimizer, + NobleClient, SubaccountClient, ValidatorConfig, OrderType, @@ -43,8 +44,10 @@ import { log } from '../telemetry'; class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { private compositeClient: CompositeClient | undefined; + private nobleClient: NobleClient | undefined; private store: RootStore | undefined; private localWallet: LocalWallet | undefined; + private nobleWallet: LocalWallet | undefined; constructor() { this.compositeClient = undefined; @@ -59,6 +62,11 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { this.localWallet = localWallet; } + setNobleWallet(nobleWallet: LocalWallet) { + this.nobleWallet = nobleWallet; + this.nobleClient?.connect(nobleWallet); + } + async connectNetwork( paramsInJson: Nullable, callback: (p0: Nullable) => void @@ -70,6 +78,7 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { websocketUrl, validatorUrl, chainId, + nobleValidatorUrl, USDC_DENOM, USDC_DECIMALS, USDC_GAS_DENOM, @@ -100,7 +109,8 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { ); this.compositeClient = compositeClient; - + this.nobleClient = new NobleClient(nobleValidatorUrl); + if (this.nobleWallet) await this.nobleClient.connect(this.nobleWallet); // Dispatch custom event to notify other parts of the app that the network has been connected const customEvent = new CustomEvent('abacus:connectNetwork', { detail: parsedParams, @@ -325,6 +335,46 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { } } + async sendNobleIBC( + params: { + msgTypeUrl: string, + msg: any, + } + ): Promise { + if (!this.nobleClient?.isConnected) { + throw new Error('Missing nobleClient or localWallet'); + } + + try { + const ibcMsg = { + typeUrl: params.msgTypeUrl, // '/ibc.applications.transfer.v1.MsgTransfer', + value: params.msg, + }; + const fee = await this.nobleClient.simulateTransaction([ibcMsg]); + + // take out fee from amount before sweeping + const amount = parseInt(ibcMsg.value.token.amount, 10) - + Math.floor(parseInt(fee.amount[0].amount, 10) * GAS_MULTIPLIER); + + if (amount <= 0) { + throw new Error('noble balance does not cover fees'); + } + + ibcMsg.value.token.amount = amount.toString(); + const tx = await this.nobleClient.send([ibcMsg]); + + const parsedTx = this.parseToPrimitives(tx); + + return JSON.stringify(parsedTx); + } catch (error) { + log('DydxChainTransactions/sendNobleIBC', error); + + return JSON.stringify({ + error, + }); + } + } + async transaction( type: TransactionTypes, paramsInJson: Abacus.Nullable, @@ -354,6 +404,11 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { callback(result); break; } + case TransactionType.SendNobleIBC: { + const result = await this.sendNobleIBC(params); + callback(result); + break; + } default: { break; } @@ -441,6 +496,13 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { const parseDelegations = this.parseToPrimitives(delegations); callback(JSON.stringify(parseDelegations)); break; + case QueryType.GetNobleBalance: + if (this.nobleClient?.isConnected) { + const nobleBalance = await this.nobleClient.getAccountBalance('uusdc'); + const parsedNobleBalance = this.parseToPrimitives(nobleBalance); + callback(JSON.stringify(parsedNobleBalance)); + } + break; default: break; } diff --git a/src/lib/abacus/index.ts b/src/lib/abacus/index.ts index c643662..4750efa 100644 --- a/src/lib/abacus/index.ts +++ b/src/lib/abacus/index.ts @@ -26,7 +26,7 @@ import { } from '@/constants/abacus'; import { DEFAULT_MARKETID } from '@/constants/markets'; -import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork } from '@/constants/networks'; +import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork, isMainnet } from '@/constants/networks'; import { CLEARED_SIZE_INPUTS, CLEARED_TRADE_INPUTS } from '@/constants/trade'; import type { RootStore } from '@/state/_store'; @@ -81,10 +81,13 @@ class AbacusStateManager { this.abacusFormatter ); + const appConfigs = AbacusAppConfig.Companion.forWeb; + if (!isMainnet) appConfigs.squidVersion = AbacusAppConfig.SquidVersion.V2DepositOnly; + this.stateManager = new AsyncAbacusStateManager( '', CURRENT_ABACUS_DEPLOYMENT, - AbacusAppConfig.Companion.forWeb, + appConfigs, ioImplementations, uiImplementations, // @ts-ignore @@ -180,6 +183,12 @@ class AbacusStateManager { } }; + setNobleWallet = (nobleWallet?: LocalWallet) => { + if (nobleWallet) { + this.chainTransactions.setNobleWallet(nobleWallet); + } + } + setTransfersSourceAddress = (evmAddress: string) => { this.stateManager.sourceAddress = evmAddress; }; diff --git a/src/lib/squid.ts b/src/lib/squid.ts new file mode 100644 index 0000000..ec2c413 --- /dev/null +++ b/src/lib/squid.ts @@ -0,0 +1,48 @@ +import { isMainnet } from '@/constants/networks'; +import { GetStatus, StatusResponse } from '@0xsquid/sdk'; + +export const NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; + +export const STATUS_ERROR_GRACE_PERIOD = 300_000; + +const getSquidStatusUrl = (isV2: boolean) => { + if (isV2) { + return isMainnet + ? 'https://v2.api.squidrouter.com/v2/status' + : 'https://testnet.v2.api.squidrouter.com/v2/status'; + } + return isMainnet + ? 'https://api.squidrouter.com/v1/status' + : 'https://testnet.api.squidrouter.com/v1/status'; +}; + +export const fetchSquidStatus = async ( + params: GetStatus, + isV2?: boolean, + integratorId?: string +): Promise => { + const parsedParams: { [key: string]: string } = { + transactionId: params.transactionId, + fromChainId: String(params.fromChainId), + toChainId: String(params.toChainId), + }; + if (isV2) parsedParams.bridgeType = 'cctp'; + const url = `${getSquidStatusUrl(!!isV2)}?${new URLSearchParams(parsedParams).toString()}`; + + const response = await fetch(url, { + headers: { + "x-integrator-id": integratorId || 'dYdX-api' + }, + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error); + } + + return response.json(); +}; + +export const getNobleChainId = () => { + return isMainnet ? 'noble-1' : 'grand-1'; +} diff --git a/src/state/inputs.ts b/src/state/inputs.ts index e8f31ef..c168c12 100644 --- a/src/state/inputs.ts +++ b/src/state/inputs.ts @@ -47,7 +47,10 @@ export const inputsSlice = createSlice({ inputErrors: errors?.toArray(), tradeInputs: trade, closePositionInputs: closePosition, - transferInputs: transfer, + transferInputs: { + ...transfer, + isCctp: !!transfer?.isCctp, + } as Nullable, }; }, diff --git a/src/views/forms/AccountManagementForms/DepositForm.tsx b/src/views/forms/AccountManagementForms/DepositForm.tsx index 84cb931..4d51f03 100644 --- a/src/views/forms/AccountManagementForms/DepositForm.tsx +++ b/src/views/forms/AccountManagementForms/DepositForm.tsx @@ -18,7 +18,6 @@ import type { EvmAddress } from '@/constants/wallets'; import { useAccounts, useDebounce, useStringGetter, useSelectedNetwork } from '@/hooks'; import { useAccountBalance, CHAIN_DEFAULT_TOKEN_ADDRESS } from '@/hooks/useAccountBalance'; import { useLocalNotifications } from '@/hooks/useLocalNotifications'; -import { NATIVE_TOKEN_ADDRESS, useSquid } from '@/hooks/useSquid'; import { layoutMixins } from '@/styles/layoutMixins'; import { formMixins } from '@/styles/formMixins'; @@ -38,6 +37,7 @@ import { getTransferInputs } from '@/state/inputsSelectors'; import abacusStateManager from '@/lib/abacus'; import { MustBigNumber } from '@/lib/numbers'; +import { getNobleChainId, NATIVE_TOKEN_ADDRESS } from '@/lib/squid'; import { log } from '@/lib/telemetry'; import { parseWalletError } from '@/lib/wallet'; @@ -69,6 +69,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { summary, errors: routeErrors, errorMessage: routeErrorMessage, + isCctp, } = useSelector(getTransferInputs, shallowEqual) || {}; const chainId = chainIdStr ? parseInt(chainIdStr) : undefined; @@ -84,7 +85,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { ); const [fromAmount, setFromAmount] = useState(''); - const [slippage, setSlippage] = useState(0.01); // 1% slippage + const [slippage, setSlippage] = useState(isCctp ? 0 : 0.01); // 1% slippage const debouncedAmount = useDebounce(fromAmount, 500); // Async Data @@ -99,6 +100,8 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { const debouncedAmountBN = MustBigNumber(debouncedAmount); const balanceBN = MustBigNumber(balance); + useEffect(() => setSlippage(isCctp ? 0 : 0.01), [isCctp]); + useEffect(() => { const hasInvalidInput = debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0) || debouncedAmountBN.gt(balanceBN); @@ -250,11 +253,11 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { if (txHash) { addTransferNotification({ txHash: txHash, - toChainId: ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId, + toChainId: !isCctp ? ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId : getNobleChainId(), fromChainId: chainIdStr || undefined, toAmount: summary?.usdcSize || undefined, triggeredAt: Date.now(), - notificationStatus: NotificationStatus.Triggered, + isCctp, }); abacusStateManager.clearTransferInputValues(); setFromAmount(''); diff --git a/src/views/forms/AccountManagementForms/DepositForm/DepositButtonAndReceipt.tsx b/src/views/forms/AccountManagementForms/DepositForm/DepositButtonAndReceipt.tsx index 4072fae..5cd5815 100644 --- a/src/views/forms/AccountManagementForms/DepositForm/DepositButtonAndReceipt.tsx +++ b/src/views/forms/AccountManagementForms/DepositForm/DepositButtonAndReceipt.tsx @@ -109,6 +109,8 @@ export const DepositButtonAndReceipt = ({ ? stringGetter({ key: STRING_KEYS.HIDE_ALL_DETAILS }) : stringGetter({ key: STRING_KEYS.SHOW_ALL_DETAILS }); + const totalFees = (summary?.bridgeFee || 0) + (summary?.gasFee || 0); + const submitButtonReceipt = [ { key: 'equity', @@ -158,9 +160,7 @@ export const DepositButtonAndReceipt = ({ { key: 'total-fees', label: {stringGetter({ key: STRING_KEYS.TOTAL_FEES })}, - value: typeof summary?.bridgeFee === 'number' && typeof summary?.gasFee === 'number' && ( - - ), + value: , subitems: feeSubitems, }, { @@ -183,7 +183,12 @@ export const DepositButtonAndReceipt = ({ type={OutputType.Text} value={stringGetter({ key: STRING_KEYS.X_MINUTES_LOWERCASED, - params: { X: Math.round(summary?.estimatedRouteDuration / 60) }, + params: { + X: + summary?.estimatedRouteDuration < 60 + ? '< 1' + : Math.round(summary?.estimatedRouteDuration / 60), + }, })} /> ), diff --git a/src/views/forms/AccountManagementForms/WithdrawForm.tsx b/src/views/forms/AccountManagementForms/WithdrawForm.tsx index 94f159c..f575366 100644 --- a/src/views/forms/AccountManagementForms/WithdrawForm.tsx +++ b/src/views/forms/AccountManagementForms/WithdrawForm.tsx @@ -58,11 +58,6 @@ export const WithdrawForm = () => { const { sendSquidWithdraw } = useSubaccount(); const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {}; - // User input - const [withdrawAmount, setWithdrawAmount] = useState(''); - const [slippage, setSlippage] = useState(0.01); // 0.1% slippage - const debouncedAmount = useDebounce(withdrawAmount, 500); - const { requestPayload, token, @@ -71,8 +66,15 @@ export const WithdrawForm = () => { resources, errors: routeErrors, errorMessage: routeErrorMessage, + isCctp } = useSelector(getTransferInputs, shallowEqual) || {}; + // User input + const [withdrawAmount, setWithdrawAmount] = useState(''); + const [slippage, setSlippage] = useState(isCctp ? 0 : 0.01); // 0.1% slippage + const debouncedAmount = useDebounce(withdrawAmount, 500); + + const isValidAddress = toAddress && isAddress(toAddress); const toToken = useMemo( @@ -89,6 +91,8 @@ export const WithdrawForm = () => { [freeCollateral?.current] ); + useEffect(() => setSlippage(isCctp ? 0 : 0.01), [isCctp]); + useEffect(() => { abacusStateManager.setTransferValue({ field: TransferInputField.type, diff --git a/src/views/forms/AccountManagementForms/WithdrawForm/WithdrawButtonAndReceipt.tsx b/src/views/forms/AccountManagementForms/WithdrawForm/WithdrawButtonAndReceipt.tsx index ff9c3f2..3a7b467 100644 --- a/src/views/forms/AccountManagementForms/WithdrawForm/WithdrawButtonAndReceipt.tsx +++ b/src/views/forms/AccountManagementForms/WithdrawForm/WithdrawButtonAndReceipt.tsx @@ -89,14 +89,14 @@ export const WithdrawButtonAndReceipt = ({ const showSubitemsToggle = showFeeBreakdown ? stringGetter({ key: STRING_KEYS.HIDE_ALL_DETAILS }) : stringGetter({ key: STRING_KEYS.SHOW_ALL_DETAILS }); + + const totalFees = (summary?.bridgeFee || 0) + (summary?.gasFee || 0); const submitButtonReceipt = [ { key: 'total-fees', label: {stringGetter({ key: STRING_KEYS.TOTAL_FEES })}, - value: typeof summary?.bridgeFee === 'number' && typeof summary?.gasFee === 'number' && ( - - ), + value: , subitems: feeSubitems, }, { @@ -165,7 +165,12 @@ export const WithdrawButtonAndReceipt = ({ type={OutputType.Text} value={stringGetter({ key: STRING_KEYS.X_MINUTES_LOWERCASED, - params: { X: Math.round(summary?.estimatedRouteDuration / 60) }, + params: { + X: + summary?.estimatedRouteDuration < 60 + ? '< 1' + : Math.round(summary?.estimatedRouteDuration / 60), + }, })} /> ), diff --git a/src/views/notifications/TransferStatusNotification/index.tsx b/src/views/notifications/TransferStatusNotification/index.tsx index e94f90b..fdbbcbf 100644 --- a/src/views/notifications/TransferStatusNotification/index.tsx +++ b/src/views/notifications/TransferStatusNotification/index.tsx @@ -44,6 +44,7 @@ export const TransferStatusNotification = ({ // @ts-ignore status.errors is not in the type definition but can be returned const error = status?.errors?.length ? status?.errors[0] : status?.error; + const hasError = error && Object.keys(error).length !== 0; const updateSecondsLeft = useCallback(() => { const fromChainEta = (status?.fromChain?.chainData?.estimatedRouteDuration || 0) * 1000; @@ -87,7 +88,7 @@ export const TransferStatusNotification = ({ }, })} - {error && ( + {hasError && ( {stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE, @@ -112,7 +113,7 @@ export const TransferStatusNotification = ({ ) : ( {content} - {!isToast && status?.squidTransactionStatus !== 'success' && ( + {!isToast && status?.squidTransactionStatus !== 'success' && !hasError && ( )} @@ -120,8 +121,7 @@ export const TransferStatusNotification = ({ } slotAction={ isToast && - status && - !error && ( + status && ( {