Skip to content

Commit

Permalink
Merge branch 'dev' of github.com:safe-global/safe-wallet-web into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
katspaugh committed Feb 13, 2024
2 parents 0852c93 + 6cd73cb commit 16d54c4
Show file tree
Hide file tree
Showing 23 changed files with 779 additions and 106 deletions.
5 changes: 4 additions & 1 deletion src/components/balances/AssetsTable/index.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -257,6 +258,8 @@ const AssetsTable = ({
<EnhancedTable rows={rows} headCells={headCells} />
</div>
)}

<CheckBalance />
</>
)
}
Expand Down
77 changes: 77 additions & 0 deletions src/components/common/CooldownButton/index.test.tsx
Original file line number Diff line number Diff line change
@@ -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 <cooldown> seconds', async () => {
const onClickEvent = jest.fn()
const result = render(
<CooldownButton cooldown={30} onClick={onClickEvent} startDisabled>
Try again
</CooldownButton>,
)

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(
<CooldownButton cooldown={30} onClick={onClickEvent}>
Try again
</CooldownButton>,
)

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)
})
})
48 changes: 48 additions & 0 deletions src/components/common/CooldownButton/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Button onClick={handleClick} variant="contained" size="small" disabled={isDisabled}>
<span>
{children}
{remainingSeconds > 0 && ` in ${Math.floor(remainingSeconds)}s`}
</span>
</Button>
)
}

export default CooldownButton
4 changes: 4 additions & 0 deletions src/components/dashboard/index.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand Down
12 changes: 7 additions & 5 deletions src/components/new-safe/ReviewRow/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Grid item xs={3}>
<Typography variant="body2">{name}</Typography>
</Grid>
<Grid item xs={9}>
{name && (
<Grid item xs={3}>
<Typography variant="body2">{name}</Typography>
</Grid>
)}
<Grid item xs={name ? 9 : 12}>
{value}
</Grid>
</>
Expand Down
27 changes: 22 additions & 5 deletions src/components/new-safe/create/logic/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<Safe> => {
export const createNewSafe = async (
ethersProvider: BrowserProvider,
props: DeploySafeProps,
safeVersion?: SafeVersion,
): Promise<Safe> => {
const ethAdapter = await createEthersAdapter(ethersProvider)

const safeFactory = await SafeFactory.create({ ethAdapter })
const safeFactory = await SafeFactory.create({ ethAdapter, safeVersion })
return safeFactory.deploySafe(props)
}

Expand Down Expand Up @@ -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()
Expand Down
112 changes: 108 additions & 4 deletions src/components/new-safe/create/steps/ReviewStep/index.test.tsx
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -25,18 +29,118 @@ 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(<NetworkFee totalFee="0" chain={mockChainInfo} willRelay={true} />)

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(<NetworkFee totalFee="0" chain={mockChainInfo} willRelay={false} />)

expect(
result.getByText(/You have used up your 5 free transactions per hour. Please try again later/),
).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(
<ReviewStep data={mockData} onSubmit={jest.fn()} onBack={jest.fn()} setStep={jest.fn()} />,
)

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(
<ReviewStep data={mockData} onSubmit={jest.fn()} onBack={jest.fn()} setStep={jest.fn()} />,
)

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(
<ReviewStep data={mockData} onSubmit={jest.fn()} onBack={jest.fn()} setStep={jest.fn()} />,
)

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(
<ReviewStep data={mockData} onSubmit={jest.fn()} onBack={jest.fn()} setStep={jest.fn()} />,
)

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(
<ReviewStep data={mockData} onSubmit={jest.fn()} onBack={jest.fn()} setStep={jest.fn()} />,
)

const payNow = getByText('Pay now')

act(() => {
fireEvent.click(payNow)
})

expect(getByText(/Who will pay gas fees:/)).toBeInTheDocument()
})
})
Loading

0 comments on commit 16d54c4

Please sign in to comment.