Skip to content

Commit

Permalink
Refactor: Upgrade ethers to v6 and protocol-kit to v2 (safe-global#3087)
Browse files Browse the repository at this point in the history
fixes safe-global#2901 (safe-global#2901)
fixes safe-global#1984 (safe-global#1984)

This PR upgrades ethers to version 6 and protocol kit to v2.

We’ve followed the migration guide from v5 to perform the update:
https://docs.ethers.org/v6/migrating/

# Ethers-v6 and protocol-kit v2 migration summary

## ethers-v6

The `tsc` target has been set to `es2020` to support the shorthand notation for BigNumber, such as `0n`.

Generally, previously imports that were made either from `@ethersproject` or let’s say `ethers/lib/utiils` are now directly exported from `ethers`. e.g.
```jsx
import { isAddress } from 'ethers/lib/utils’
```

becomes

```jsx
import { isAddress } from 'ethers’
```

Imports from `@safe-global/safe-core-sdk` are now coming from `@safe-global/protocol-kit`

Some packages rely on ethers v^6.7.1, others on v^6.9.2 . Yarn somehow fails to resolve those to the same ethers version, that’s why in package.json we force the resolution such packages to a specific version that we use. Otherwise we are getting weird typescript errors that types are incompatible, where in reality they should be compatible

Some methods have been made `async` as in ethers-v6 there is no longer an `contract.address` prop, instead one needs to call `await contract.getAddress()` to get the address. Mainly because of this in a lot of places where `useEffect` was used, we now use the `useAsync` hook.

`safe-ethers-lib` has been removed and replaced by `imports form @safe-global/protocol-kit`.

`@safe-global/safe-core-sdk-utils` has been removed.

Mapping functions from v5 → v6:

`hexZeroPad` → `toBeHex`
`hexZeroPad` → `zeroPadValue` (the difference to `toBeHex` is that the provided value needs to have even lengh, otherwise we get an error)

`BigNumber.from(value)` → `BigInt(value)` or short notation `0n`, `1n`, `2n` etc 

*note: BigInt shouldn’t come in JSX, otherwise one gets an error. Stuff like this is no longer valid:*

`{myBigNumber && <div>something</div>}`

has to be ⇒ 

`{Number(myBigNumber) && <div> something</div>}`

`Web3Provider` → `BrowserProvider`

`arraify` → `getBytes`

`hexDataLength` → `dataLength`

`type PayableOverrides` → `type Overrides`

`defaultAbiCoder` → `AbiCoder.defaultAbiCoder()`

`_TypedDataEncoder` → `TypedDataEncoder`

`ethers.utils.solidityPack` → `solidityPacked`

`hexValue` → `toQuantity`

`contract.callStatic.(execTransaction|whateverFunction)` → `contract.execTransaction.staticCall`

 

## protocol-kit

`isL1SafeMasterCopy` → `isL1SafeSingleton`

This mock:

```jsx
jest.mock('@safe-global/protocol-kit', () => {
const originalModule = jest.requireActual('@safe-global/protocol-kit')

// Mock class
class MockEthersAdapter extends originalModule.EthersAdapter {
getChainId = jest.fn().mockImplementation(() => Promise.resolve(BigInt(4)))
}

return {
...originalModule,
EthersAdapter: MockEthersAdapter,
}
})
```

Is now needed in all tests that touch protocol-kit’s `EthersAdapter`. `EthersAdapter` automatically tries to get the chain id (in the past we were providing it to it), because of this we have to mock the `getChainId` function, otherwise we get a mysterous error “Range error” in the tests.

---------------------------------------------
Co-authored-by: Daniel Dimitrov <daniel.d@safe-global>
Co-authored-by: Usame Algan <usame@safe-global>
Co-authored-by: Manuel Gellfart <manu@safe.global>
  • Loading branch information
compojoom authored Jan 25, 2024
1 parent 5ee1aff commit 3d7b0b4
Show file tree
Hide file tree
Showing 195 changed files with 2,349 additions and 1,889 deletions.
3 changes: 2 additions & 1 deletion cypress/e2e/pages/create_wallet.pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ export function selectNetwork(network) {
cy.wait(1000)
cy.get(expandMoreIcon).eq(1).parents('div').eq(1).click()
cy.wait(1000)
cy.get('li').contains(network).click()
let regex = new RegExp(`^${network}$`)
cy.get('li').contains(regex).click()
}

export function clickOnNextBtn() {
Expand Down
11 changes: 11 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,14 @@ Object.defineProperty = (obj, prop, desc) => {
}
return defineProperty(obj, prop, desc)
}

// We need this, otherwise jest will fail with:
// invalid BytesLike value on createRandom
// https://github.com/ethers-io/ethers.js/issues/4365
Object.defineProperty(Uint8Array, Symbol.hasInstance, {
value(potentialInstance) {
return this === Uint8Array
? Object.prototype.toString.call(potentialInstance) === '[object Uint8Array]'
: Uint8Array[Symbol.hasInstance].call(this, potentialInstance)
},
})
2 changes: 1 addition & 1 deletion next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const nextConfig = {
reactStrictMode: false,
productionBrowserSourceMaps: true,
eslint: {
dirs: ['src'],
dirs: ['src', 'cypress'],
},
experimental: {
optimizePackageImports: ['@mui/material', '@mui/icons-material', 'lodash', 'date-fns', '@sentry/react', '@gnosis.pm/zodiac'],
Expand Down
22 changes: 12 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"cmp": "./scripts/cmp.sh",
"routes": "node scripts/generate-routes.js > src/config/routes.ts && prettier -w src/config/routes.ts && cat src/config/routes.ts",
"css-vars": "npx -y tsx ./scripts/css-vars.ts > ./src/styles/vars.css && prettier -w src/styles/vars.css",
"generate-types": "typechain --target ethers-v5 --out-dir src/types/contracts ./node_modules/@safe-global/safe-deployments/dist/assets/**/*.json ./node_modules/@safe-global/safe-modules-deployments/dist/assets/**/*.json ./node_modules/@openzeppelin/contracts/build/contracts/ERC20.json ./node_modules/@openzeppelin/contracts/build/contracts/ERC721.json",
"generate-types": "typechain --target ethers-v6 --out-dir src/types/contracts ./node_modules/@safe-global/safe-deployments/dist/assets/**/*.json ./node_modules/@safe-global/safe-modules-deployments/dist/assets/**/*.json ./node_modules/@openzeppelin/contracts/build/contracts/ERC20.json ./node_modules/@openzeppelin/contracts/build/contracts/ERC721.json",
"after-install": "yarn generate-types",
"postinstall": "yarn after-install",
"analyze": "cross-env ANALYZE=true yarn build",
Expand All @@ -36,24 +36,26 @@
"@walletconnect/core": "^2.10.6",
"@walletconnect/ethereum-provider": "^2.10.6",
"@walletconnect/utils": "^2.10.6",
"@web3-onboard/trezor/**/protobufjs": "^7.2.4"
"@web3-onboard/trezor/**/protobufjs": "^7.2.4",
"@safe-global/safe-core-sdk-types/**/ethers": "^6.10.0",
"@safe-global/protocol-kit/**/ethers": "^6.10.0",
"@safe-global/api-kit/**/ethers": "^6.10.0"
},
"dependencies": {
"@ducanh2912/next-pwa": "^9.7.1",
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.0",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@gnosis.pm/zodiac": "^3.4.2",
"@gnosis.pm/zodiac": "^4.0.1",
"@mui/icons-material": "^5.14.20",
"@mui/material": "^5.14.20",
"@mui/x-date-pickers": "^5.0.20",
"@reduxjs/toolkit": "^1.9.5",
"@safe-global/api-kit": "^2.0.0",
"@safe-global/protocol-kit": "^2.0.0",
"@safe-global/safe-apps-sdk": "^9.0.0-next.1",
"@safe-global/safe-core-sdk": "^3.3.5",
"@safe-global/safe-core-sdk-utils": "^1.7.4",
"@safe-global/safe-deployments": "1.28.0",
"@safe-global/safe-ethers-lib": "^1.9.4",
"@safe-global/safe-gateway-typescript-sdk": "^3.14.0",
"@safe-global/safe-modules-deployments": "^1.2.0",
"@sentry/react": "^7.91.0",
Expand All @@ -74,7 +76,7 @@
"bn.js": "^5.2.1",
"classnames": "^2.3.1",
"date-fns": "^2.30.0",
"ethers": "5.7.2",
"ethers": "^6.10.0",
"exponential-backoff": "^3.1.0",
"firebase": "^10.3.1",
"framer-motion": "^10.13.1",
Expand All @@ -99,14 +101,14 @@
"@faker-js/faker": "^8.1.0",
"@next/bundle-analyzer": "^13.5.6",
"@openzeppelin/contracts": "^4.9.2",
"@safe-global/safe-core-sdk-types": "^1.9.1",
"@safe-global/safe-core-sdk-types": "^3.0.1",
"@sentry/types": "^7.74.0",
"@svgr/webpack": "^6.3.1",
"@testing-library/cypress": "^8.0.7",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.4.2",
"@typechain/ethers-v5": "^10.2.0",
"@typechain/ethers-v6": "^0.5.1",
"@types/jest": "^29.5.4",
"@types/js-cookie": "^3.0.2",
"@types/lodash": "^4.14.182",
Expand All @@ -133,7 +135,7 @@
"jest-environment-jsdom": "^29.6.2",
"prettier": "^2.7.0",
"ts-prune": "^0.10.3",
"typechain": "^8.0.0",
"typechain": "^8.3.2",
"typescript": "4.9.4",
"typescript-plugin-css-modules": "^4.2.2",
"webpack": "^5.88.2"
Expand Down
34 changes: 17 additions & 17 deletions src/components/balances/AssetsTable/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { TOKEN_LISTS } from '@/store/settingsSlice'
import { act, fireEvent, getByRole, getByTestId, render, waitFor } from '@/tests/test-utils'
import { safeParseUnits } from '@/utils/formatters'
import { TokenType } from '@safe-global/safe-gateway-typescript-sdk'
import { hexZeroPad } from 'ethers/lib/utils'
import { toBeHex } from 'ethers'
import { useState } from 'react'
import AssetsTable from '.'
import { COLLAPSE_TIMEOUT_MS } from './useHideAssets'
Expand Down Expand Up @@ -45,7 +45,7 @@ describe('AssetsTable', () => {

test('select and deselect hidden assets', async () => {
const mockHiddenAssets = {
'5': [hexZeroPad('0x2', 20), hexZeroPad('0x3', 20)],
'5': [toBeHex('0x2', 20), toBeHex('0x3', 20)],
}
const mockBalances = {
data: {
Expand All @@ -56,7 +56,7 @@ describe('AssetsTable', () => {
fiatBalance: '100',
fiatConversion: '1',
tokenInfo: {
address: hexZeroPad('0x2', 20),
address: toBeHex('0x2', 20),
decimals: 18,
logoUri: '',
name: 'DAI',
Expand All @@ -69,7 +69,7 @@ describe('AssetsTable', () => {
fiatBalance: '200',
fiatConversion: '1',
tokenInfo: {
address: hexZeroPad('0x3', 20),
address: toBeHex('0x3', 20),
decimals: 18,
logoUri: '',
name: 'SPAM',
Expand Down Expand Up @@ -151,7 +151,7 @@ describe('AssetsTable', () => {

test('Deselect all and save', async () => {
const mockHiddenAssets = {
'5': [hexZeroPad('0x2', 20), hexZeroPad('0x3', 20), hexZeroPad('0xdead', 20)],
'5': [toBeHex('0x2', 20), toBeHex('0x3', 20), toBeHex('0xdead', 20)],
}
const mockBalances = {
data: {
Expand All @@ -162,7 +162,7 @@ describe('AssetsTable', () => {
fiatBalance: '100',
fiatConversion: '1',
tokenInfo: {
address: hexZeroPad('0x2', 20),
address: toBeHex('0x2', 20),
decimals: 18,
logoUri: '',
name: 'DAI',
Expand All @@ -175,7 +175,7 @@ describe('AssetsTable', () => {
fiatBalance: '200',
fiatConversion: '1',
tokenInfo: {
address: hexZeroPad('0x3', 20),
address: toBeHex('0x3', 20),
decimals: 18,
logoUri: '',
name: 'SPAM',
Expand Down Expand Up @@ -230,9 +230,9 @@ describe('AssetsTable', () => {
})

// Expect 3 hidden token addresses
expect(result.queryByText(hexZeroPad('0x2', 20))).not.toBeNull()
expect(result.queryByText(hexZeroPad('0x3', 20))).not.toBeNull()
expect(result.queryByText(hexZeroPad('0xdead', 20))).not.toBeNull()
expect(result.queryByText(toBeHex('0x2', 20))).not.toBeNull()
expect(result.queryByText(toBeHex('0x3', 20))).not.toBeNull()
expect(result.queryByText(toBeHex('0xdead', 20))).not.toBeNull()

fireEvent.click(result.getByText('Deselect all'))
fireEvent.click(result.getByText('Save'))
Expand All @@ -246,9 +246,9 @@ describe('AssetsTable', () => {
})

// Expect one hidden token, which was not part of the current balance
expect(result.queryByText(hexZeroPad('0x2', 20))).toBeNull()
expect(result.queryByText(hexZeroPad('0x3', 20))).toBeNull()
expect(result.queryByText(hexZeroPad('0xdead', 20))).not.toBeNull()
expect(result.queryByText(toBeHex('0x2', 20))).toBeNull()
expect(result.queryByText(toBeHex('0x3', 20))).toBeNull()
expect(result.queryByText(toBeHex('0xdead', 20))).not.toBeNull()
})

test('immediately hide visible assets', async () => {
Expand All @@ -264,7 +264,7 @@ describe('AssetsTable', () => {
fiatBalance: '100',
fiatConversion: '1',
tokenInfo: {
address: hexZeroPad('0x2', 20),
address: toBeHex('0x2', 20),
decimals: 18,
logoUri: '',
name: 'DAI',
Expand All @@ -277,7 +277,7 @@ describe('AssetsTable', () => {
fiatBalance: '200',
fiatConversion: '1',
tokenInfo: {
address: hexZeroPad('0x3', 20),
address: toBeHex('0x3', 20),
decimals: 18,
logoUri: '',
name: 'SPAM',
Expand Down Expand Up @@ -363,7 +363,7 @@ describe('AssetsTable', () => {
fiatBalance: '100',
fiatConversion: '1',
tokenInfo: {
address: hexZeroPad('0x2', 20),
address: toBeHex('0x2', 20),
decimals: 18,
logoUri: '',
name: 'DAI',
Expand All @@ -376,7 +376,7 @@ describe('AssetsTable', () => {
fiatBalance: '200',
fiatConversion: '1',
tokenInfo: {
address: hexZeroPad('0x3', 20),
address: toBeHex('0x3', 20),
decimals: 18,
logoUri: '',
name: 'SPAM',
Expand Down
8 changes: 4 additions & 4 deletions src/components/balances/HiddenTokenButton/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as useChainId from '@/hooks/useChainId'
import { fireEvent, render } from '@/tests/test-utils'
import { hexZeroPad } from 'ethers/lib/utils'
import { toBeHex } from 'ethers'
import { TokenType } from '@safe-global/safe-gateway-typescript-sdk'
import { safeParseUnits } from '@/utils/formatters'
import HiddenTokenButton from '.'
Expand All @@ -23,7 +23,7 @@ describe('HiddenTokenToggle', () => {

test('button disabled if hidden assets are visible', async () => {
const mockHiddenAssets = {
'5': [hexZeroPad('0x3', 20)],
'5': [toBeHex('0x3', 20)],
}
const mockBalances = {
data: {
Expand All @@ -34,7 +34,7 @@ describe('HiddenTokenToggle', () => {
fiatBalance: '100',
fiatConversion: '1',
tokenInfo: {
address: hexZeroPad('0x2', 20),
address: toBeHex('0x2', 20),
decimals: 18,
logoUri: '',
name: 'DAI',
Expand All @@ -47,7 +47,7 @@ describe('HiddenTokenToggle', () => {
fiatBalance: '200',
fiatConversion: '1',
tokenInfo: {
address: hexZeroPad('0x3', 20),
address: toBeHex('0x3', 20),
decimals: 18,
logoUri: '',
name: 'SPAM',
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactNode, ReactElement, SyntheticEvent } from 'react'
import { isAddress } from 'ethers/lib/utils'
import { isAddress } from 'ethers'
import { useTheme } from '@mui/material/styles'
import Box from '@mui/material/Box'
import useMediaQuery from '@mui/material/useMediaQuery'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import * as nextRouter from 'next/router'
import useChainId from '@/hooks/useChainId'
import { render, waitFor } from '@/tests/test-utils'
import { SafeAppAccessPolicyTypes } from '@safe-global/safe-gateway-typescript-sdk'
import { BigNumber } from 'ethers'
import SafeTokenWidget from '..'
import { hexZeroPad } from 'ethers/lib/utils'
import { toBeHex } from 'ethers'
import { AppRoutes } from '@/config/routes'
import useSafeTokenAllocation, { useSafeVotingPower } from '@/hooks/useSafeTokenAllocation'

Expand Down Expand Up @@ -37,7 +36,7 @@ jest.mock(
)

describe('SafeTokenWidget', () => {
const fakeSafeAddress = hexZeroPad('0x1', 20)
const fakeSafeAddress = toBeHex('0x1', 20)
beforeEach(() => {
jest.restoreAllMocks()
jest.spyOn(nextRouter, 'useRouter').mockImplementation(
Expand All @@ -53,23 +52,23 @@ describe('SafeTokenWidget', () => {
it('Should render nothing for unsupported chains', () => {
;(useChainId as jest.Mock).mockImplementationOnce(jest.fn(() => '100'))
;(useSafeTokenAllocation as jest.Mock).mockImplementation(() => [[], , false])
;(useSafeVotingPower as jest.Mock).mockImplementation(() => [BigNumber.from(0), , false])
;(useSafeVotingPower as jest.Mock).mockImplementation(() => [BigInt(0), , false])

const result = render(<SafeTokenWidget />)
expect(result.baseElement).toContainHTML('<body><div /></body>')
})

it('Should display 0 if Safe has no SAFE token', async () => {
;(useSafeTokenAllocation as jest.Mock).mockImplementation(() => [[], , false])
;(useSafeVotingPower as jest.Mock).mockImplementation(() => [BigNumber.from(0), , false])
;(useSafeVotingPower as jest.Mock).mockImplementation(() => [BigInt(0), , false])

const result = render(<SafeTokenWidget />)
await waitFor(() => expect(result.baseElement).toHaveTextContent('0'))
})

it('Should display the value formatted correctly', async () => {
;(useSafeTokenAllocation as jest.Mock).mockImplementation(() => [[], , false])
;(useSafeVotingPower as jest.Mock).mockImplementation(() => [BigNumber.from('472238796133701648384'), , false])
;(useSafeVotingPower as jest.Mock).mockImplementation(() => [BigInt('472238796133701648384'), , false])

// to avoid failing tests in some environments
const NumberFormat = Intl.NumberFormat
Expand All @@ -86,7 +85,7 @@ describe('SafeTokenWidget', () => {

it('Should render a link to the governance app', async () => {
;(useSafeTokenAllocation as jest.Mock).mockImplementation(() => [[], , false])
;(useSafeVotingPower as jest.Mock).mockImplementation(() => [BigNumber.from(420000), , false])
;(useSafeVotingPower as jest.Mock).mockImplementation(() => [BigInt(420000), , false])

const result = render(<SafeTokenWidget />)
await waitFor(() => {
Expand All @@ -98,7 +97,7 @@ describe('SafeTokenWidget', () => {

it('Should render a claim button for SEP5 qualification', async () => {
;(useSafeTokenAllocation as jest.Mock).mockImplementation(() => [[{ tag: 'user_v2' }], , false])
;(useSafeVotingPower as jest.Mock).mockImplementation(() => [BigNumber.from(420000), , false])
;(useSafeVotingPower as jest.Mock).mockImplementation(() => [BigInt(420000), , false])

const result = render(<SafeTokenWidget />)
await waitFor(() => {
Expand Down
3 changes: 1 addition & 2 deletions src/components/common/SafeTokenWidget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import useSafeTokenAllocation, { useSafeVotingPower, type Vesting } from '@/hook
import { OVERVIEW_EVENTS } from '@/services/analytics'
import { formatVisualAmount } from '@/utils/formatters'
import { Box, Button, ButtonBase, Skeleton, Tooltip, Typography } from '@mui/material'
import { BigNumber } from 'ethers'
import Link from 'next/link'
import { useRouter } from 'next/router'
import type { UrlObject } from 'url'
Expand Down Expand Up @@ -61,7 +60,7 @@ const SafeTokenWidget = () => {
: undefined

const canRedeemSep5 = canRedeemSep5Airdrop(allocationData)
const flooredSafeBalance = formatVisualAmount(allocation || BigNumber.from(0), TOKEN_DECIMALS, 2)
const flooredSafeBalance = formatVisualAmount(allocation || BigInt(0), TOKEN_DECIMALS, 2)

return (
<Box className={css.container}>
Expand Down
5 changes: 2 additions & 3 deletions src/components/common/TokenAmountInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import NumberField from '@/components/common/NumberField'
import { validateDecimalLength, validateLimitedAmount } from '@/utils/validation'
import { AutocompleteItem } from '@/components/tx-flow/flows/TokenTransfer/CreateTokenTransfer'
import { useFormContext } from 'react-hook-form'
import { type BigNumber } from '@ethersproject/bignumber'
import classNames from 'classnames'
import { useCallback } from 'react'

Expand All @@ -23,7 +22,7 @@ const TokenAmountInput = ({
}: {
balances: SafeBalanceResponse['items']
selectedToken: SafeBalanceResponse['items'][number] | undefined
maxAmount?: BigNumber
maxAmount?: bigint
validate?: (value: string) => string | undefined
}) => {
const {
Expand Down Expand Up @@ -68,7 +67,7 @@ const TokenAmountInput = ({
variant="standard"
InputProps={{
disableUnderline: true,
endAdornment: maxAmount && (
endAdornment: maxAmount !== undefined && Number(maxAmount) && (
<Button data-testid="max-btn" className={css.max} onClick={onMaxAmountClick}>
Max
</Button>
Expand Down
Loading

0 comments on commit 3d7b0b4

Please sign in to comment.