Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
Eternal Safe is a decentralized fork of [Safe{Wallet}](https://github.com/safe-global/safe-wallet-monorepo), forked at v1.26.2. Funded by the [Safe Grants Program](https://app.charmverse.io/safe-grants-program/page-005239065690887612).

- The latest released version is always accessible at [https://eternalsafe.eth](https://eternalsafe.eth). If your browser doesn't support ENS, you can use alternatives below with different privacy trade-offs:
- [https://eternalsafe.eth.limo](https://eternalsafe.eth.limo) - centralized ENS resolution.
- [https://eternalsafe.eth.limo](https://eternalsafe.eth.limo) - centralized ENS resolution.
- [https://eternalsafe-eth.ipns.inbrowser.link](https://eternalsafe-eth.ipns.inbrowser.link) - this [fetches and verifies client-side](https://inbrowser.link/) the IPFS content.
- [https://eternalsafe.earthfast.app](https://eternalsafe.earthfast.app) - [EarthFast (an IPFS alternative)](https://earthfast.com) hosts a mirror.
- [https://eternalsafe.earthfast.app](https://eternalsafe.earthfast.app) - [EarthFast (an IPFS alternative)](https://earthfast.com) hosts a mirror.
- For the IPFS CID or pinned ENS subdomain, please check the [latest release](https://github.com/eternalsafe/wallet/releases/latest).
- The latest commit on the `eternalsafe` branch is always accessible at [https://eternalsafe.vercel.app](https://eternalsafe.vercel.app).

Expand Down Expand Up @@ -68,7 +68,7 @@ Here's the list of all the environment variables:
| Env variable | Description |
| --------------------------- | ----------------------------------------------------------------------------- |
| `NEXT_PUBLIC_IS_PRODUCTION` | Set to `true` to build a minified production app |
| `NEXT_PUBLIC_SAFE_VERSION` | The latest version of the Safe contract, defaults to 1.3.0 |
| `NEXT_PUBLIC_SAFE_VERSION` | The latest version of the Safe contract, defaults to 1.4.1 |
| `NEXT_PUBLIC_WC_PROJECT_ID` | [WalletConnect v2](https://docs.walletconnect.com/2.0/cloud/relay) project ID |

If you don't provide some of the variables, the corresponding features will be disabled in the UI.
Expand Down
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"static-serve": "yarn build && yarn serve"
},
"engines": {
"node": ">=16"
"node": ">=20.18.0"
},
"resolutions": {
"@solana/web3.js": "^1.87.7",
Expand Down Expand Up @@ -60,13 +60,13 @@
"@uniswap/token-lists": "1.0.0-beta.33",
"@walletconnect/utils": "^2.10.6",
"@walletconnect/web3wallet": "^1.9.5",
"@web3-onboard/coinbase": "^2.2.6",
"@web3-onboard/core": "^2.21.2",
"@web3-onboard/injected-wallets": "^2.10.7",
"@web3-onboard/keystone": "^2.3.7",
"@web3-onboard/ledger": "2.3.2",
"@web3-onboard/trezor": "^2.4.2",
"@web3-onboard/walletconnect": "^2.5.2",
"@web3-onboard/coinbase": "^2.2.7",
"@web3-onboard/core": "^2.21.6",
"@web3-onboard/injected-wallets": "^2.10.17",
"@web3-onboard/keystone": "^2.3.8",
"@web3-onboard/ledger": "2.7.1",
"@web3-onboard/trezor": "^2.4.4",
"@web3-onboard/walletconnect": "^2.5.5",
"blo": "^1.1.1",
"bn.js": "^5.2.1",
"classnames": "^2.3.1",
Expand Down Expand Up @@ -127,7 +127,7 @@
"prettier": "^2.7.0",
"ts-prune": "^0.10.3",
"typechain": "^8.0.0",
"typescript": "4.9.4",
"typescript": "^5.0.4",
"typescript-plugin-css-modules": "^4.2.2",
"webpack": "^5.88.2"
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/NetworkSelector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { type ReactElement, useMemo } from 'react'
import { useCallback } from 'react'
import { AppRoutes } from '@/config/routes'

const keepPathRoutes = [AppRoutes.welcome.index, AppRoutes.newSafe.load]
const keepPathRoutes = [AppRoutes.welcome.index, AppRoutes.newSafe.load, AppRoutes.newSafe.create]

const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => {
const { configs } = useChains()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import * as usePendingSafe from '../steps/StatusStep/usePendingSafe'
import * as useIsWrongChain from '@/hooks/useIsWrongChain'
import * as useRouter from 'next/router'
import { type NextRouter } from 'next/router'
import { AppRoutes } from '@/config/routes'

describe('useSyncSafeCreationStep', () => {
const mockPendingSafe = {
Expand All @@ -23,21 +22,6 @@ describe('useSyncSafeCreationStep', () => {
jest.clearAllMocks()
})

it('should go to the first step if no wallet is connected and there is no pending safe', async () => {
const mockPushRoute = jest.fn()
jest.spyOn(wallet, 'default').mockReturnValue(null)
jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([undefined, setPendingSafeSpy])
jest.spyOn(useRouter, 'useRouter').mockReturnValue({
push: mockPushRoute,
} as unknown as NextRouter)
const mockSetStep = jest.fn()

renderHook(() => useSyncSafeCreationStep(mockSetStep))

expect(mockSetStep).not.toHaveBeenCalled()
expect(mockPushRoute).toHaveBeenCalledWith({ pathname: AppRoutes.welcome.index, query: undefined })
})

it('should go to the fourth step if there is a pending safe', async () => {
const mockPushRoute = jest.fn()
jest.spyOn(localStorage, 'default').mockReturnValue([{}, jest.fn()])
Expand Down
21 changes: 8 additions & 13 deletions src/components/new-safe/create/logic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ import { backOff } from 'exponential-backoff'
import { LATEST_SAFE_VERSION } from '@/config/constants'
import { EMPTY_DATA, ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants'
import { formatError } from '@/utils/formatters'
import { getSafeSDKAndImplementation } from '@/hooks/coreSDK/useInitSafeCoreSDK'
import { getSafeSDKAndImplementation, getSafeAddressFromTxReceipt } from '@/hooks/coreSDK/useInitSafeCoreSDK'
import type { Provider } from '@ethersproject/providers'
import { getSafeInfo } from '@/hooks/loadables/useLoadSafeInfo'
import type { SafeVersion } from '@safe-global/safe-core-sdk-types'

export type SafeCreationProps = {
owners: string[]
Expand Down Expand Up @@ -63,20 +64,10 @@ export const getSafeDeployProps = async (
export const createNewSafe = async (ethersProvider: Web3Provider, props: DeploySafeProps): Promise<Safe> => {
const ethAdapter = createEthersAdapter(ethersProvider)

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

/**
* Compute the new counterfactual Safe address before it is actually created
*/
export const computeNewSafeAddress = async (ethersProvider: Web3Provider, props: DeploySafeProps): Promise<string> => {
const ethAdapter = createEthersAdapter(ethersProvider)

const safeFactory = await SafeFactory.create({ ethAdapter })
return safeFactory.predictSafeAddress(props.safeAccountConfig, props.saltNonce)
}

/**
* Encode a Safe creation transaction NOT using the Core SDK because it doesn't support that
* This is used for gas estimation.
Expand Down Expand Up @@ -151,10 +142,14 @@ export const estimateSafeCreationGas = async (
})
}

export const pollSafeInfo = async (web3: Provider, chainId: string, safeAddress: string): Promise<SafeInfo> => {
export const pollSafeInfo = async (web3: Provider, chainId: string, txHash: string): Promise<SafeInfo> => {
// exponential delay between attempts for around 4 min
return backOff(
async () => {
const safeAddress = await getSafeAddressFromTxReceipt(txHash, web3)
if (!safeAddress) {
throw new Error('Safe address not found in transaction receipt')
}
let [sdk, implementation] = await getSafeSDKAndImplementation(web3, safeAddress, chainId)
if (!sdk) {
throw new Error('Safe SDK not available')
Expand Down
19 changes: 0 additions & 19 deletions src/components/new-safe/create/steps/ReviewStep/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardS
import type { NewSafeFormData } from '@/components/new-safe/create'
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 useSyncSafeCreationStep from '@/components/new-safe/create/useSyncSafeCreationStep'
Expand All @@ -24,7 +22,6 @@ import useIsWrongChain from '@/hooks/useIsWrongChain'
import ReviewRow from '@/components/new-safe/ReviewRow'
import { BigNumber } from 'ethers'
import { usePendingSafe } from '../StatusStep/usePendingSafe'
import { LATEST_SAFE_VERSION } from '@/config/constants'
import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'

export const NetworkFee = ({ totalFee, chain }: { totalFee: string; chain: ChainInfo | undefined }) => {
Expand Down Expand Up @@ -92,25 +89,9 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps<NewSafe
const createSafe = async () => {
if (!wallet || !provider || !chain) return

const readOnlyFallbackHandlerContract = await getReadOnlyFallbackHandlerContract(chain.chainId, LATEST_SAFE_VERSION)

const props = {
safeAccountConfig: {
threshold: data.threshold,
owners: data.owners.map((owner) => owner.address),
fallbackHandler: readOnlyFallbackHandlerContract.getAddress(),
},
safeDeploymentConfig: {
saltNonce: saltNonce.toString(),
},
}

const safeAddress = await computeNewSafeAddress(provider, props)

const pendingSafe = {
...data,
saltNonce,
safeAddress,
}

setPendingSafe(pendingSafe)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ describe('useSafeCreationEffects', () => {
const setPendingSafeSpy = jest.fn()
jest
.spyOn(usePendingSafe, 'usePendingSafe')
.mockReturnValue([{ safeAddress: hexZeroPad('0x123', 20) } as PendingSafeData, setPendingSafeSpy])
.mockReturnValue([
{ safeAddress: hexZeroPad('0x123', 20), txHash: '0x123' } as PendingSafeData,
setPendingSafeSpy,
])
renderHook(() =>
useSafeCreationEffects({
status: SafeCreationStatus.SUCCESS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@ const useSafeCreationEffects = ({

// Asynchronously wait for Safe creation
useEffect(() => {
if (status === SafeCreationStatus.SUCCESS && pendingSafe?.safeAddress && web3ReadOnly) {
pollSafeInfo(web3ReadOnly, chainId, pendingSafe.safeAddress)
.then(() => setStatus(SafeCreationStatus.INDEXED))
if (status === SafeCreationStatus.SUCCESS && pendingSafe?.txHash && web3ReadOnly) {
pollSafeInfo(web3ReadOnly, chainId, pendingSafe.txHash)
.then((data) => {
setPendingSafe({ ...pendingSafe, safeAddress: data.address.value })
setStatus(SafeCreationStatus.INDEXED)
})
.catch(() => setStatus(SafeCreationStatus.INDEX_FAILED))
}
}, [chainId, pendingSafe?.safeAddress, web3ReadOnly, status, setStatus])
}, [chainId, pendingSafe?.txHash, web3ReadOnly, status, setStatus, setPendingSafe, pendingSafe])

// Warn about leaving the page before Safe creation
useEffect(() => {
Expand Down
6 changes: 0 additions & 6 deletions src/components/new-safe/create/useSyncSafeCreationStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import useWallet from '@/hooks/wallets/useWallet'
import { usePendingSafe } from './steps/StatusStep/usePendingSafe'
import useIsWrongChain from '@/hooks/useIsWrongChain'
import { useRouter } from 'next/router'
import { AppRoutes } from '@/config/routes'

const useSyncSafeCreationStep = (setStep: StepRenderProps<NewSafeFormData>['setStep']) => {
const [pendingSafe] = usePendingSafe()
Expand All @@ -20,11 +19,6 @@ const useSyncSafeCreationStep = (setStep: StepRenderProps<NewSafeFormData>['setS
return
}

// Jump to the welcome page if there is no wallet
if (!wallet) {
router.push({ pathname: AppRoutes.welcome.index, query: router.query })
}

// Jump to choose name and network step if the wallet is connected to the wrong chain and there is no pending Safe
if (isWrongChain) {
setStep(0)
Expand Down
2 changes: 1 addition & 1 deletion src/components/transactions/TxDetails/SafeTxGasForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const SafeTxGasForm = () => {
const { safeTx, safeTxGas = 0 } = useContext(SafeTxContext)
const { safe } = useSafeInfo()
const isOldSafe = safe.version && isLegacyVersion(safe.version)
const isEditable = safeTx?.signatures.size === 0 && (safeTxGas > 0 || isOldSafe)
const isEditable = safeTx?.signatures.size === 0 && (Number(safeTxGas) > 0 || isOldSafe)
const [editing, setEditing] = useState(false)

return (
Expand Down
19 changes: 19 additions & 0 deletions src/components/welcome/WelcomeLogin/NewSafe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Button } from '@mui/material'
import { AppRoutes } from '@/config/routes'

const NewSafe = () => {
return (
<Button
href={AppRoutes.newSafe.create}
sx={{ minHeight: '42px' }}
variant="contained"
size="small"
disableElevation
fullWidth
>
Create Safe
</Button>
)
}

export default NewSafe
8 changes: 6 additions & 2 deletions src/components/welcome/WelcomeLogin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import LoadRPCUrl from '@/components/welcome/WelcomeLogin/LoadRPCUrl'
import { CHAINLIST_URL } from '@/config/constants'
import { useEffect, useState } from 'react'
import { useWeb3 } from '@/hooks/wallets/web3'
import NewSafe from './NewSafe'

const WelcomeLogin = () => {
const web3 = useWeb3()
Expand Down Expand Up @@ -35,9 +36,12 @@ const WelcomeLogin = () => {
{(web3 || customRpcUrl) && !forceShowRpcInput ? (
<>
<Typography mb={2} textAlign="center">
Eternal Safe does not yet support creating a Safe, you must have one already created.
Create a new Safe or load an existing one.
</Typography>
<LoadSafe />
<Box display="flex" flexDirection="column" gap={2}>
<LoadSafe />
<NewSafe />
</Box>
{/* TODO(eternalsafe): Allow import of data here */}
</>
) : chain ? (
Expand Down
1 change: 1 addition & 0 deletions src/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const AppRoutes = {
},
newSafe: {
load: '/new-safe/load',
create: '/new-safe/create',
},
settings: {
setup: '/settings/setup',
Expand Down
28 changes: 28 additions & 0 deletions src/hooks/coreSDK/useInitSafeCoreSDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,34 @@ export const getSafeImplementation = async (web3: Provider, safeAddress: string,
})
}

// Helper function to extract Safe address from transaction receipt
export const getSafeAddressFromTxReceipt = async (txHash: string, web3: Provider): Promise<string> => {
const receipt = await web3.getTransactionReceipt(txHash)
const proxyCreationEvents = receipt.logs.filter((log) => {
return log.topics.includes(ethers.utils.id('ProxyCreation(address,address)'))
})

if (proxyCreationEvents.length > 0) {
if (proxyCreationEvents[0].topics.length == 1) {
// pre 1.4.1
const proxyAddressData = proxyCreationEvents[0].data
const proxyAddress = ethers.utils.getAddress(
ethers.utils.defaultAbiCoder.decode(['address'], proxyAddressData)[0],
)
return proxyAddress
} else {
// 1.4.1 and later
const proxyAddressTopic = proxyCreationEvents[0].topics[1]
const proxyAddress = ethers.utils.getAddress(
ethers.utils.defaultAbiCoder.decode(['address'], proxyAddressTopic)[0],
)
return proxyAddress
}
}

throw new Error('Safe address not found in transaction receipt')
}

export const getSafeSDKAndImplementation = async (
web3: Provider,
safeAddress: string,
Expand Down
Loading
Loading