diff --git a/src/components/balances/AssetsTable/index.tsx b/src/components/balances/AssetsTable/index.tsx
index 224983b867..db035340b0 100644
--- a/src/components/balances/AssetsTable/index.tsx
+++ b/src/components/balances/AssetsTable/index.tsx
@@ -1,3 +1,4 @@
+import CheckBalance from '@/features/counterfactual/CheckBalance'
import { type ReactElement, useMemo, useContext } from 'react'
import { Button, Tooltip, Typography, SvgIcon, IconButton, Box, Checkbox, Skeleton } from '@mui/material'
import type { TokenInfo } from '@safe-global/safe-gateway-typescript-sdk'
@@ -138,7 +139,7 @@ const AssetsTable = ({
[hiddenAssets, balances.items, showHiddenAssets],
)
- const hasNoAssets = balances.items.length === 1 && balances.items[0].balance === '0'
+ const hasNoAssets = !loading && balances.items.length === 1 && balances.items[0].balance === '0'
const selectedAssetCount = visibleAssets?.filter((item) => isAssetSelected(item.tokenInfo.address)).length || 0
@@ -257,6 +258,8 @@ const AssetsTable = ({
)}
+
+
>
)
}
diff --git a/src/components/common/CooldownButton/index.test.tsx b/src/components/common/CooldownButton/index.test.tsx
new file mode 100644
index 0000000000..7de9dc9eed
--- /dev/null
+++ b/src/components/common/CooldownButton/index.test.tsx
@@ -0,0 +1,77 @@
+import { render, waitFor } from '@/tests/test-utils'
+import CooldownButton from './index'
+
+describe('CooldownButton', () => {
+ beforeAll(() => {
+ jest.useFakeTimers()
+ })
+
+ afterAll(() => {
+ jest.useRealTimers()
+ })
+ it('should be disabled initially if startDisabled is set and become enabled after seconds', async () => {
+ const onClickEvent = jest.fn()
+ const result = render(
+
+ Try again
+ ,
+ )
+
+ expect(result.getByRole('button')).toBeDisabled()
+ expect(result.getByText('Try again in 30s')).toBeVisible()
+
+ jest.advanceTimersByTime(10_000)
+
+ await waitFor(() => {
+ expect(result.getByRole('button')).toBeDisabled()
+ expect(result.getByText('Try again in 20s')).toBeVisible()
+ })
+
+ jest.advanceTimersByTime(5_000)
+
+ await waitFor(() => {
+ expect(result.getByRole('button')).toBeDisabled()
+ expect(result.getByText('Try again in 15s')).toBeVisible()
+ })
+
+ jest.advanceTimersByTime(15_000)
+
+ await waitFor(() => {
+ expect(result.getByRole('button')).toBeEnabled()
+ })
+ result.getByRole('button').click()
+
+ expect(onClickEvent).toHaveBeenCalledTimes(1)
+ await waitFor(() => {
+ expect(result.getByRole('button')).toBeDisabled()
+ })
+ })
+
+ it('should be enabled initially if startDisabled is not set and become disabled after click', async () => {
+ const onClickEvent = jest.fn()
+ const result = render(
+
+ Try again
+ ,
+ )
+
+ expect(result.getByRole('button')).toBeEnabled()
+ result.getByRole('button').click()
+
+ expect(onClickEvent).toHaveBeenCalledTimes(1)
+
+ await waitFor(() => {
+ expect(result.getByRole('button')).toBeDisabled()
+ expect(result.getByText('Try again in 30s')).toBeVisible()
+ })
+
+ jest.advanceTimersByTime(30_000)
+
+ await waitFor(() => {
+ expect(result.getByRole('button')).toBeEnabled()
+ })
+ result.getByRole('button').click()
+
+ expect(onClickEvent).toHaveBeenCalledTimes(2)
+ })
+})
diff --git a/src/components/common/CooldownButton/index.tsx b/src/components/common/CooldownButton/index.tsx
new file mode 100644
index 0000000000..aee9af0714
--- /dev/null
+++ b/src/components/common/CooldownButton/index.tsx
@@ -0,0 +1,48 @@
+import { Button } from '@mui/material'
+import { useState, useCallback, useEffect, type ReactNode } from 'react'
+
+// TODO: Extract into a hook so it can be reused for links and not just buttons
+const CooldownButton = ({
+ onClick,
+ cooldown,
+ startDisabled = false,
+ children,
+}: {
+ onClick: () => void
+ startDisabled?: boolean
+ cooldown: number // Cooldown in seconds
+ children: ReactNode
+}) => {
+ const [remainingSeconds, setRemainingSeconds] = useState(startDisabled ? cooldown : 0)
+ const [lastSendTime, setLastSendTime] = useState(startDisabled ? Date.now() : 0)
+
+ const adjustSeconds = useCallback(() => {
+ const remainingCoolDownSeconds = Math.max(0, cooldown * 1000 - (Date.now() - lastSendTime)) / 1000
+ setRemainingSeconds(remainingCoolDownSeconds)
+ }, [cooldown, lastSendTime])
+
+ useEffect(() => {
+ // Counter for progress
+ const interval = setInterval(adjustSeconds, 1000)
+ return () => clearInterval(interval)
+ }, [adjustSeconds])
+
+ const handleClick = () => {
+ setLastSendTime(Date.now())
+ setRemainingSeconds(cooldown)
+ onClick()
+ }
+
+ const isDisabled = remainingSeconds > 0
+
+ return (
+
+ )
+}
+
+export default CooldownButton
diff --git a/src/components/dashboard/index.tsx b/src/components/dashboard/index.tsx
index 75d8d96613..faebec1afa 100644
--- a/src/components/dashboard/index.tsx
+++ b/src/components/dashboard/index.tsx
@@ -1,3 +1,4 @@
+import useOnboard from '@/hooks/wallets/useOnboard'
import type { ReactElement } from 'react'
import dynamic from 'next/dynamic'
import { Grid } from '@mui/material'
@@ -17,8 +18,11 @@ const RecoveryWidget = dynamic(() => import('@/features/recovery/components/Reco
const Dashboard = (): ReactElement => {
const router = useRouter()
+ const onboard = useOnboard()
const { [CREATION_MODAL_QUERY_PARM]: showCreationModal = '' } = router.query
+ console.log(onboard)
+
const supportsRecovery = useIsRecoverySupported()
const [recovery] = useRecovery()
const showRecoveryWidget = supportsRecovery && !recovery
diff --git a/src/components/new-safe/ReviewRow/index.tsx b/src/components/new-safe/ReviewRow/index.tsx
index 7478c054c4..6f434c108c 100644
--- a/src/components/new-safe/ReviewRow/index.tsx
+++ b/src/components/new-safe/ReviewRow/index.tsx
@@ -1,13 +1,15 @@
import React, { type ReactElement } from 'react'
import { Grid, Typography } from '@mui/material'
-const ReviewRow = ({ name, value }: { name: string; value: ReactElement }) => {
+const ReviewRow = ({ name, value }: { name?: string; value: ReactElement }) => {
return (
<>
-
- {name}
-
-
+ {name && (
+
+ {name}
+
+ )}
+
{value}
>
diff --git a/src/components/new-safe/create/logic/index.ts b/src/components/new-safe/create/logic/index.ts
index 6273208db0..cff9bf4e14 100644
--- a/src/components/new-safe/create/logic/index.ts
+++ b/src/components/new-safe/create/logic/index.ts
@@ -1,3 +1,4 @@
+import type { SafeVersion } from '@safe-global/safe-core-sdk-types'
import { type BrowserProvider, type Provider } from 'ethers'
import { getSafeInfo, type SafeInfo, type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'
@@ -59,10 +60,14 @@ export const getSafeDeployProps = async (
/**
* Create a Safe creation transaction via Core SDK and submits it to the wallet
*/
-export const createNewSafe = async (ethersProvider: BrowserProvider, props: DeploySafeProps): Promise => {
+export const createNewSafe = async (
+ ethersProvider: BrowserProvider,
+ props: DeploySafeProps,
+ safeVersion?: SafeVersion,
+): Promise => {
const ethAdapter = await createEthersAdapter(ethersProvider)
- const safeFactory = await SafeFactory.create({ ethAdapter })
+ const safeFactory = await SafeFactory.create({ ethAdapter, safeVersion })
return safeFactory.deploySafe(props)
}
@@ -280,10 +285,22 @@ export const getRedirect = (
return redirectUrl + `${appendChar}safe=${address}`
}
-export const relaySafeCreation = async (chain: ChainInfo, owners: string[], threshold: number, saltNonce: number) => {
- const readOnlyProxyFactoryContract = await getReadOnlyProxyFactoryContract(chain.chainId, LATEST_SAFE_VERSION)
+export const relaySafeCreation = async (
+ chain: ChainInfo,
+ owners: string[],
+ threshold: number,
+ saltNonce: number,
+ safeVersion?: SafeVersion,
+) => {
+ const readOnlyProxyFactoryContract = await getReadOnlyProxyFactoryContract(
+ chain.chainId,
+ safeVersion ?? LATEST_SAFE_VERSION,
+ )
const proxyFactoryAddress = await readOnlyProxyFactoryContract.getAddress()
- const readOnlyFallbackHandlerContract = await getReadOnlyFallbackHandlerContract(chain.chainId, LATEST_SAFE_VERSION)
+ const readOnlyFallbackHandlerContract = await getReadOnlyFallbackHandlerContract(
+ chain.chainId,
+ safeVersion ?? LATEST_SAFE_VERSION,
+ )
const fallbackHandlerAddress = await readOnlyFallbackHandlerContract.getAddress()
const readOnlySafeContract = await getReadOnlyGnosisSafeContract(chain)
const safeContractAddress = await readOnlySafeContract.getAddress()
diff --git a/src/components/new-safe/create/steps/ReviewStep/index.test.tsx b/src/components/new-safe/create/steps/ReviewStep/index.test.tsx
index 397e4bc18a..8ebfc9bdaa 100644
--- a/src/components/new-safe/create/steps/ReviewStep/index.test.tsx
+++ b/src/components/new-safe/create/steps/ReviewStep/index.test.tsx
@@ -1,10 +1,14 @@
+import type { NewSafeFormData } from '@/components/new-safe/create'
+import * as useChains from '@/hooks/useChains'
+import * as relay from '@/utils/relaying'
import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'
import { render } from '@/tests/test-utils'
-import { NetworkFee } from '@/components/new-safe/create/steps/ReviewStep/index'
+import ReviewStep, { NetworkFee } from '@/components/new-safe/create/steps/ReviewStep/index'
import * as useWallet from '@/hooks/wallets/useWallet'
import { type ConnectedWallet } from '@/hooks/wallets/useOnboard'
-import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule'
+import * as socialLogin from '@/services/mpc/SocialLoginModule'
+import { act, fireEvent } from '@testing-library/react'
const mockChainInfo = {
chainId: '100',
@@ -25,14 +29,14 @@ describe('NetworkFee', () => {
})
it('displays a sponsored by message for social login', () => {
- jest.spyOn(useWallet, 'default').mockReturnValue({ label: ONBOARD_MPC_MODULE_LABEL } as unknown as ConnectedWallet)
+ jest.spyOn(useWallet, 'default').mockReturnValue({ label: 'Social Login' } as unknown as ConnectedWallet)
const result = render()
expect(result.getByText(/Your account is sponsored by Gnosis/)).toBeInTheDocument()
})
it('displays an error message for social login if there are no relays left', () => {
- jest.spyOn(useWallet, 'default').mockReturnValue({ label: ONBOARD_MPC_MODULE_LABEL } as unknown as ConnectedWallet)
+ jest.spyOn(useWallet, 'default').mockReturnValue({ label: 'Social Login' } as unknown as ConnectedWallet)
const result = render()
expect(
@@ -40,3 +44,103 @@ describe('NetworkFee', () => {
).toBeInTheDocument()
})
})
+
+describe('ReviewStep', () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it('should display a pay now pay later option for counterfactual safe setups', () => {
+ const mockData: NewSafeFormData = {
+ name: 'Test',
+ threshold: 1,
+ owners: [{ name: '', address: '0x1' }],
+ saltNonce: 0,
+ }
+ jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true)
+
+ const { getByText } = render(
+ ,
+ )
+
+ expect(getByText('Pay now')).toBeInTheDocument()
+ })
+
+ it('should not display the network fee for counterfactual safes', () => {
+ const mockData: NewSafeFormData = {
+ name: 'Test',
+ threshold: 1,
+ owners: [{ name: '', address: '0x1' }],
+ saltNonce: 0,
+ }
+ jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true)
+
+ const { queryByText } = render(
+ ,
+ )
+
+ expect(queryByText('You will have to confirm a transaction and pay an estimated fee')).not.toBeInTheDocument()
+ })
+
+ it('should not display the execution method for counterfactual safes', () => {
+ const mockData: NewSafeFormData = {
+ name: 'Test',
+ threshold: 1,
+ owners: [{ name: '', address: '0x1' }],
+ saltNonce: 0,
+ }
+ jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true)
+
+ const { queryByText } = render(
+ ,
+ )
+
+ expect(queryByText('Who will pay gas fees:')).not.toBeInTheDocument()
+ })
+
+ it('should display the network fee for counterfactual safes if the user selects pay now', async () => {
+ const mockData: NewSafeFormData = {
+ name: 'Test',
+ threshold: 1,
+ owners: [{ name: '', address: '0x1' }],
+ saltNonce: 0,
+ }
+ jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true)
+
+ const { getByText } = render(
+ ,
+ )
+
+ const payNow = getByText('Pay now')
+
+ act(() => {
+ fireEvent.click(payNow)
+ })
+
+ expect(getByText(/You will have to confirm a transaction and pay an estimated fee/)).toBeInTheDocument()
+ })
+
+ it('should display the execution method for counterfactual safes if the user selects pay now and there is relaying', async () => {
+ const mockData: NewSafeFormData = {
+ name: 'Test',
+ threshold: 1,
+ owners: [{ name: '', address: '0x1' }],
+ saltNonce: 0,
+ }
+ jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true)
+ jest.spyOn(relay, 'hasRemainingRelays').mockReturnValue(true)
+ jest.spyOn(socialLogin, 'isSocialLoginWallet').mockReturnValue(false)
+
+ const { getByText } = render(
+ ,
+ )
+
+ const payNow = getByText('Pay now')
+
+ act(() => {
+ fireEvent.click(payNow)
+ })
+
+ expect(getByText(/Who will pay gas fees:/)).toBeInTheDocument()
+ })
+})
diff --git a/src/components/new-safe/create/steps/ReviewStep/index.tsx b/src/components/new-safe/create/steps/ReviewStep/index.tsx
index 585939ed23..dee132db0c 100644
--- a/src/components/new-safe/create/steps/ReviewStep/index.tsx
+++ b/src/components/new-safe/create/steps/ReviewStep/index.tsx
@@ -1,52 +1,55 @@
-import { getAvailableSaltNonce } from '@/components/new-safe/create/logic/utils'
-import type { NamedAddress } from '@/components/new-safe/create/types'
-import ErrorMessage from '@/components/tx/ErrorMessage'
-import { createCounterfactualSafe } from '@/features/counterfactual/utils'
-import useWalletCanPay from '@/hooks/useWalletCanPay'
-import { useAppDispatch } from '@/store'
-import { FEATURES } from '@/utils/chains'
-import { useRouter } from 'next/router'
-import { useMemo, useState } from 'react'
-import { Button, Grid, Typography, Divider, Box, Alert } from '@mui/material'
-import lightPalette from '@/components/theme/lightPalette'
import ChainIndicator from '@/components/common/ChainIndicator'
+import type { NamedAddress } from '@/components/new-safe/create/types'
import EthHashInfo from '@/components/common/EthHashInfo'
-import { useCurrentChain, useHasFeature } from '@/hooks/useChains'
-import useGasPrice, { getTotalFeeFormatted } from '@/hooks/useGasPrice'
-import { useEstimateSafeCreationGas } from '@/components/new-safe/create/useEstimateSafeCreationGas'
+import { getTotalFeeFormatted } from '@/hooks/useGasPrice'
import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper'
import type { NewSafeFormData } from '@/components/new-safe/create'
+import { computeNewSafeAddress } from '@/components/new-safe/create/logic'
+import { getAvailableSaltNonce } from '@/components/new-safe/create/logic/utils'
+import NetworkWarning from '@/components/new-safe/create/NetworkWarning'
import css from '@/components/new-safe/create/steps/ReviewStep/styles.module.css'
import layoutCss from '@/components/new-safe/create/styles.module.css'
-import { getReadOnlyFallbackHandlerContract } from '@/services/contracts/safeContracts'
-import { computeNewSafeAddress } from '@/components/new-safe/create/logic'
-import useWallet from '@/hooks/wallets/useWallet'
-import { useWeb3 } from '@/hooks/wallets/web3'
+import { useEstimateSafeCreationGas } from '@/components/new-safe/create/useEstimateSafeCreationGas'
import useSyncSafeCreationStep from '@/components/new-safe/create/useSyncSafeCreationStep'
-import ArrowBackIcon from '@mui/icons-material/ArrowBack'
-import NetworkWarning from '@/components/new-safe/create/NetworkWarning'
-import useIsWrongChain from '@/hooks/useIsWrongChain'
import ReviewRow from '@/components/new-safe/ReviewRow'
-import { ExecutionMethodSelector, ExecutionMethod } from '@/components/tx/ExecutionMethodSelector'
-import { MAX_HOUR_RELAYS, useLeastRemainingRelays } from '@/hooks/useRemainingRelays'
-import classnames from 'classnames'
-import { hasRemainingRelays } from '@/utils/relaying'
-import { usePendingSafe } from '../StatusStep/usePendingSafe'
+import ErrorMessage from '@/components/tx/ErrorMessage'
+import { ExecutionMethod, ExecutionMethodSelector } from '@/components/tx/ExecutionMethodSelector'
+import { RELAY_SPONSORS } from '@/components/tx/SponsoredBy'
import { LATEST_SAFE_VERSION } from '@/config/constants'
+import PayNowPayLater, { PayMethod } from '@/features/counterfactual/PayNowPayLater'
+import { createCounterfactualSafe } from '@/features/counterfactual/utils'
+import { useCurrentChain, useHasFeature } from '@/hooks/useChains'
+import useGasPrice from '@/hooks/useGasPrice'
+import useIsWrongChain from '@/hooks/useIsWrongChain'
+import { MAX_HOUR_RELAYS, useLeastRemainingRelays } from '@/hooks/useRemainingRelays'
+import useWalletCanPay from '@/hooks/useWalletCanPay'
+import useWallet from '@/hooks/wallets/useWallet'
+import { useWeb3 } from '@/hooks/wallets/web3'
+import { getReadOnlyFallbackHandlerContract } from '@/services/contracts/safeContracts'
import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule'
-import { RELAY_SPONSORS } from '@/components/tx/SponsoredBy'
-import Image from 'next/image'
-import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'
+import { useAppDispatch } from '@/store'
+import { FEATURES } from '@/utils/chains'
+import { hasRemainingRelays } from '@/utils/relaying'
+import ArrowBackIcon from '@mui/icons-material/ArrowBack'
+import { Alert, Box, Button, Divider, Grid, Typography } from '@mui/material'
import { type DeploySafeProps } from '@safe-global/protocol-kit'
+import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'
+import classnames from 'classnames'
+import Image from 'next/image'
+import { useRouter } from 'next/router'
+import { useMemo, useState } from 'react'
+import { usePendingSafe } from '../StatusStep/usePendingSafe'
export const NetworkFee = ({
totalFee,
chain,
willRelay,
+ inline = false,
}: {
totalFee: string
chain: ChainInfo | undefined
willRelay: boolean
+ inline?: boolean
}) => {
const wallet = useWallet()
@@ -54,16 +57,8 @@ export const NetworkFee = ({
if (!isSocialLogin) {
return (
-
-
+
+
≈ {totalFee} {chain?.nativeCurrency.symbol}
@@ -156,6 +151,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps()
const isCounterfactualEnabled = useHasFeature(FEATURES.COUNTERFACTUAL)
@@ -211,7 +207,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps
+ {isCounterfactual && (
+ <>
+
+
+
+
+ {canRelay && !isSocialLogin && payMethod === PayMethod.PayNow && (
+
+
+ }
+ />
+
+ )}
+
+ {payMethod === PayMethod.PayNow && (
+
+
+ You will have to confirm a transaction and pay an estimated fee of{' '}
+ with your connected
+ wallet
+
+
+ )}
+
+ >
+ )}
+
{!isCounterfactual && (
<>
diff --git a/src/components/new-safe/create/steps/ReviewStep/styles.module.css b/src/components/new-safe/create/steps/ReviewStep/styles.module.css
index 2d4e1be420..9b87174229 100644
--- a/src/components/new-safe/create/steps/ReviewStep/styles.module.css
+++ b/src/components/new-safe/create/steps/ReviewStep/styles.module.css
@@ -13,3 +13,16 @@
.errorMessage {
margin-top: 0;
}
+
+.networkFee {
+ padding: var(--space-1);
+ background-color: var(--color-secondary-background);
+ color: var(--color-static-main);
+ width: fit-content;
+ border-radius: 6px;
+}
+
+.networkFeeInline {
+ padding: 2px 4px;
+ display: inline-flex;
+}
diff --git a/src/components/sidebar/Sidebar/index.tsx b/src/components/sidebar/Sidebar/index.tsx
index e6d0128553..d34dfa39c7 100644
--- a/src/components/sidebar/Sidebar/index.tsx
+++ b/src/components/sidebar/Sidebar/index.tsx
@@ -1,5 +1,5 @@
import { useCallback, useState, type ReactElement } from 'react'
-import { Box, Divider, Drawer, IconButton } from '@mui/material'
+import { Box, Divider, Drawer } from '@mui/material'
import ChevronRight from '@mui/icons-material/ChevronRight'
import ChainIndicator from '@/components/common/ChainIndicator'
@@ -31,9 +31,9 @@ const Sidebar = (): ReactElement => {
{/* Open the safes list */}
-
+
+
{/* Address, balance, copy button, etc */}
diff --git a/src/components/sidebar/Sidebar/styles.module.css b/src/components/sidebar/Sidebar/styles.module.css
index fc200c3691..f5f3013f33 100644
--- a/src/components/sidebar/Sidebar/styles.module.css
+++ b/src/components/sidebar/Sidebar/styles.module.css
@@ -46,6 +46,8 @@
.drawerButton {
position: absolute !important;
z-index: 2;
+ color: var(--color-text-primary);
+ padding: 8px 0;
right: 0;
transform: translateX(50%);
margin-top: 54px;
diff --git a/src/features/counterfactual/ActivateAccount.tsx b/src/features/counterfactual/ActivateAccount.tsx
index 860316236f..27101f6fa9 100644
--- a/src/features/counterfactual/ActivateAccount.tsx
+++ b/src/features/counterfactual/ActivateAccount.tsx
@@ -11,6 +11,7 @@ import ErrorMessage from '@/components/tx/ErrorMessage'
import { ExecutionMethod, ExecutionMethodSelector } from '@/components/tx/ExecutionMethodSelector'
import useDeployGasLimit from '@/features/counterfactual/hooks/useDeployGasLimit'
import { removeUndeployedSafe, selectUndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice'
+import { CF_TX_GROUP_KEY, showSubmitNotification } from '@/features/counterfactual/utils'
import useChainId from '@/hooks/useChainId'
import { useCurrentChain } from '@/hooks/useChains'
import useGasPrice, { getTotalFeeFormatted } from '@/hooks/useGasPrice'
@@ -22,6 +23,7 @@ import useWallet from '@/hooks/wallets/useWallet'
import { useWeb3 } from '@/hooks/wallets/web3'
import { asError } from '@/services/exceptions/utils'
import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule'
+import { txDispatch, TxEvent } from '@/services/tx/txEvents'
import { waitForCreateSafeTx } from '@/services/tx/txMonitor'
import { useAppDispatch, useAppSelector } from '@/store'
import { hasFeature } from '@/utils/chains'
@@ -44,12 +46,12 @@ const useActivateAccount = () => {
? {
maxFeePerGas: maxFeePerGas?.toString(),
maxPriorityFeePerGas: maxPriorityFeePerGas?.toString(),
- gasLimit: gasLimit?.toString(),
+ gasLimit: gasLimit?.totalGas.toString(),
}
- : { gasPrice: maxFeePerGas?.toString(), gasLimit: gasLimit?.toString() }
+ : { gasPrice: maxFeePerGas?.toString(), gasLimit: gasLimit?.totalGas.toString() }
- const totalFee = getTotalFeeFormatted(maxFeePerGas, maxPriorityFeePerGas, gasLimit, chain)
- const walletCanPay = useWalletCanPay({ gasLimit, maxFeePerGas, maxPriorityFeePerGas })
+ const totalFee = getTotalFeeFormatted(maxFeePerGas, maxPriorityFeePerGas, gasLimit?.totalGas, chain)
+ const walletCanPay = useWalletCanPay({ gasLimit: gasLimit?.totalGas, maxFeePerGas, maxPriorityFeePerGas })
return { options, totalFee, walletCanPay }
}
@@ -81,6 +83,12 @@ const ActivateAccountFlow = () => {
const onSuccess = () => {
dispatch(removeUndeployedSafe({ chainId, address: safeAddress }))
+ txDispatch(TxEvent.SUCCESS, { groupKey: CF_TX_GROUP_KEY })
+ }
+
+ const onSubmit = (txHash?: string) => {
+ showSubmitNotification(dispatch, chain, txHash)
+ setTxFlow(undefined)
}
const createSafe = async () => {
@@ -96,19 +104,28 @@ const ActivateAccountFlow = () => {
undeployedSafe.safeAccountConfig.owners,
undeployedSafe.safeAccountConfig.threshold,
Number(undeployedSafe.safeDeploymentConfig?.saltNonce!),
+ undeployedSafe.safeDeploymentConfig?.safeVersion,
)
+ onSubmit()
+
waitForCreateSafeTx(taskId, (status) => {
if (status === SafeCreationStatus.SUCCESS) {
onSuccess()
}
})
} else {
- await createNewSafe(provider, {
- safeAccountConfig: undeployedSafe.safeAccountConfig,
- saltNonce: undeployedSafe.safeDeploymentConfig?.saltNonce,
- options,
- })
+ await createNewSafe(
+ provider,
+ {
+ safeAccountConfig: undeployedSafe.safeAccountConfig,
+ saltNonce: undeployedSafe.safeDeploymentConfig?.saltNonce,
+ options,
+
+ callback: onSubmit,
+ },
+ undeployedSafe.safeDeploymentConfig?.safeVersion,
+ )
onSuccess()
}
} catch (_err) {
@@ -117,8 +134,6 @@ const ActivateAccountFlow = () => {
setSubmitError(err)
return
}
-
- setTxFlow(undefined)
}
const submitDisabled = !isSubmittable
diff --git a/src/features/counterfactual/CheckBalance.tsx b/src/features/counterfactual/CheckBalance.tsx
new file mode 100644
index 0000000000..b8e2c97229
--- /dev/null
+++ b/src/features/counterfactual/CheckBalance.tsx
@@ -0,0 +1,43 @@
+import CooldownButton from '@/components/common/CooldownButton'
+import ExternalLink from '@/components/common/ExternalLink'
+import { getCounterfactualBalance } from '@/features/counterfactual/utils'
+import { useCurrentChain } from '@/hooks/useChains'
+import useSafeInfo from '@/hooks/useSafeInfo'
+import { useWeb3 } from '@/hooks/wallets/web3'
+import { getBlockExplorerLink } from '@/utils/chains'
+import { Box, Typography } from '@mui/material'
+
+const CheckBalance = () => {
+ const { safe, safeAddress } = useSafeInfo()
+ const chain = useCurrentChain()
+ const provider = useWeb3()
+
+ if (safe.deployed) return null
+
+ const handleBalanceRefresh = async () => {
+ void getCounterfactualBalance(safeAddress, provider, chain, true)
+ }
+
+ const blockExplorerLink = chain ? getBlockExplorerLink(chain, safeAddress) : undefined
+
+ return (
+
+
+ Don't see your tokens?
+
+
+
+ Refresh balance
+
+ or
+ {blockExplorerLink && (
+
+ check on Block Explorer
+
+ )}
+
+
+ )
+}
+
+export default CheckBalance
diff --git a/src/features/counterfactual/CounterfactualForm.tsx b/src/features/counterfactual/CounterfactualForm.tsx
index f2423be182..a4b3753086 100644
--- a/src/features/counterfactual/CounterfactualForm.tsx
+++ b/src/features/counterfactual/CounterfactualForm.tsx
@@ -3,6 +3,7 @@ import useDeployGasLimit from '@/features/counterfactual/hooks/useDeployGasLimit
import { removeUndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice'
import { deploySafeAndExecuteTx } from '@/features/counterfactual/utils'
import useChainId from '@/hooks/useChainId'
+import { getTotalFeeFormatted } from '@/hooks/useGasPrice'
import useSafeInfo from '@/hooks/useSafeInfo'
import useWalletCanPay from '@/hooks/useWalletCanPay'
import useOnboard from '@/hooks/wallets/useOnboard'
@@ -10,7 +11,7 @@ import useWallet from '@/hooks/wallets/useWallet'
import { useAppDispatch } from '@/store'
import madProps from '@/utils/mad-props'
import React, { type ReactElement, type SyntheticEvent, useContext, useState } from 'react'
-import { CircularProgress, Box, Button, CardActions, Divider } from '@mui/material'
+import { CircularProgress, Box, Button, CardActions, Divider, Alert } from '@mui/material'
import classNames from 'classnames'
import ErrorMessage from '@/components/tx/ErrorMessage'
@@ -46,6 +47,7 @@ export const CounterfactualForm = ({
}): ReactElement => {
const wallet = useWallet()
const onboard = useOnboard()
+ const chain = useCurrentChain()
const chainId = useChainId()
const dispatch = useAppDispatch()
const { safeAddress } = useSafeInfo()
@@ -61,7 +63,7 @@ export const CounterfactualForm = ({
// Estimate gas limit
const { gasLimit, gasLimitError } = useDeployGasLimit(safeTx)
- const [advancedParams, setAdvancedParams] = useAdvancedParams(gasLimit)
+ const [advancedParams, setAdvancedParams] = useAdvancedParams(gasLimit?.totalGas)
// On modal submit
const handleSubmit = async (e: SyntheticEvent) => {
@@ -91,12 +93,11 @@ export const CounterfactualForm = ({
return
}
- // TODO: Show a success or status screen
setTxFlow(undefined)
}
const walletCanPay = useWalletCanPay({
- gasLimit,
+ gasLimit: gasLimit?.totalGas,
maxFeePerGas: advancedParams.maxFeePerGas,
maxPriorityFeePerGas: advancedParams.maxPriorityFeePerGas,
})
@@ -113,6 +114,30 @@ export const CounterfactualForm = ({
return (
<>