diff --git a/package.json b/package.json
index b3180f4caa..927e0f644b 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,7 @@
"@safe-global/protocol-kit": "^4.1.3",
"@safe-global/safe-apps-sdk": "^9.1.0",
"@safe-global/safe-client-gateway-sdk": "v1.60.1",
- "@safe-global/safe-gateway-typescript-sdk": "3.22.4",
+ "@safe-global/safe-gateway-typescript-sdk": "3.22.5-beta.0",
"@safe-global/safe-modules-deployments": "^2.2.1",
"@sentry/react": "^7.91.0",
"@spindl-xyz/attribution-lite": "^1.4.0",
diff --git a/src/components/common/ExternalLink/index.tsx b/src/components/common/ExternalLink/index.tsx
index b70a61b347..c32b9f8004 100644
--- a/src/components/common/ExternalLink/index.tsx
+++ b/src/components/common/ExternalLink/index.tsx
@@ -22,6 +22,7 @@ const ExternalLink = ({
display: 'inline-flex',
alignItems: 'center',
gap: 0.2,
+ cursor: 'pointer',
}}
>
{children}
diff --git a/src/components/common/NetworkSelector/__tests__/NetworkMultiSelector.test.tsx b/src/components/common/NetworkSelector/__tests__/NetworkMultiSelector.test.tsx
index bba2e2f360..23119e62aa 100644
--- a/src/components/common/NetworkSelector/__tests__/NetworkMultiSelector.test.tsx
+++ b/src/components/common/NetworkSelector/__tests__/NetworkMultiSelector.test.tsx
@@ -32,31 +32,36 @@ describe('NetworkMultiSelector', () => {
.with({ chainId: '1' })
.with({ chainName: 'Ethereum' })
.with({ shortName: 'eth' })
- .with({ features: [FEATURES.SAFE_141, FEATURES.COUNTERFACTUAL, FEATURES.MULTI_CHAIN_SAFE_CREATION] as any })
+ .with({ features: [FEATURES.COUNTERFACTUAL, FEATURES.MULTI_CHAIN_SAFE_CREATION] as any })
+ .with({ recommendedMasterCopyVersion: '1.4.1' })
.build(),
chainBuilder()
.with({ chainId: '10' })
.with({ chainName: 'Optimism' })
.with({ shortName: 'oeth' })
- .with({ features: [FEATURES.SAFE_141, FEATURES.COUNTERFACTUAL, FEATURES.MULTI_CHAIN_SAFE_CREATION] as any })
+ .with({ features: [FEATURES.COUNTERFACTUAL, FEATURES.MULTI_CHAIN_SAFE_CREATION] as any })
+ .with({ recommendedMasterCopyVersion: '1.4.1' })
.build(),
chainBuilder()
.with({ chainId: '100' })
.with({ chainName: 'Gnosis Chain' })
.with({ shortName: 'gno' })
- .with({ features: [FEATURES.SAFE_141, FEATURES.COUNTERFACTUAL, FEATURES.MULTI_CHAIN_SAFE_CREATION] as any })
+ .with({ features: [FEATURES.COUNTERFACTUAL, FEATURES.MULTI_CHAIN_SAFE_CREATION] as any })
+ .with({ recommendedMasterCopyVersion: '1.4.1' })
.build(),
chainBuilder()
.with({ chainId: '324' })
.with({ chainName: 'ZkSync Era' })
.with({ shortName: 'zksync' })
- .with({ features: [FEATURES.SAFE_141, FEATURES.COUNTERFACTUAL] as any })
+ .with({ features: [FEATURES.COUNTERFACTUAL] as any })
+ .with({ recommendedMasterCopyVersion: '1.4.1' })
.build(),
chainBuilder()
.with({ chainId: '480' })
.with({ chainName: 'Worldchain' })
.with({ shortName: 'wc' })
- .with({ features: [FEATURES.SAFE_141, FEATURES.COUNTERFACTUAL, FEATURES.MULTI_CHAIN_SAFE_CREATION] as any })
+ .with({ features: [FEATURES.COUNTERFACTUAL, FEATURES.MULTI_CHAIN_SAFE_CREATION] as any })
+ .with({ recommendedMasterCopyVersion: '1.4.1' })
.build(),
]
diff --git a/src/components/new-safe/create/logic/index.test.ts b/src/components/new-safe/create/logic/index.test.ts
index eaab210a00..4870901e89 100644
--- a/src/components/new-safe/create/logic/index.test.ts
+++ b/src/components/new-safe/create/logic/index.test.ts
@@ -22,7 +22,6 @@ import {
getReadOnlyProxyFactoryContract,
} from '@/services/contracts/safeContracts'
import { FEATURES, getLatestSafeVersion } from '@/utils/chains'
-import { type FEATURES as GatewayFeatures } from '@safe-global/safe-gateway-typescript-sdk'
import { chainBuilder } from '@/tests/builders/chains'
import { type ReplayedSafeProps } from '@/store/slices'
import { faker } from '@faker-js/faker'
@@ -39,9 +38,7 @@ import { Safe_to_l2_setup__factory } from '@/types/contracts'
const provider = new JsonRpcProvider(undefined, { name: 'ethereum', chainId: 1 })
const latestSafeVersion = getLatestSafeVersion(
- chainBuilder()
- .with({ chainId: '1', features: [FEATURES.SAFE_141 as unknown as GatewayFeatures] })
- .build(),
+ chainBuilder().with({ chainId: '1', recommendedMasterCopyVersion: '1.4.1' }).build(),
)
const safeToL2SetupDeployment = getSafeToL2SetupDeployment()
@@ -57,7 +54,7 @@ describe('create/logic', () => {
.with({
chainId: '1',
l2: false,
- features: [FEATURES.SAFE_141 as unknown as GatewayFeatures],
+ recommendedMasterCopyVersion: '1.4.1',
})
.build()
@@ -209,7 +206,7 @@ describe('create/logic', () => {
chainBuilder()
.with({ chainId: '1' })
// Multichain creation is toggled off
- .with({ features: [FEATURES.SAFE_141, FEATURES.COUNTERFACTUAL] as any })
+ .with({ features: [FEATURES.COUNTERFACTUAL] as any })
.with({ l2: false })
.build(),
),
@@ -239,7 +236,8 @@ describe('create/logic', () => {
chainBuilder()
.with({ chainId: '137' })
// Multichain creation is toggled off
- .with({ features: [FEATURES.SAFE_141, FEATURES.COUNTERFACTUAL] as any })
+ .with({ features: [FEATURES.COUNTERFACTUAL] as any })
+ .with({ recommendedMasterCopyVersion: '1.4.1' })
.with({ l2: true })
.build(),
),
@@ -269,7 +267,8 @@ describe('create/logic', () => {
chainBuilder()
.with({ chainId: '137' })
// Multichain creation is toggled on
- .with({ features: [FEATURES.SAFE_141, FEATURES.COUNTERFACTUAL, FEATURES.MULTI_CHAIN_SAFE_CREATION] as any })
+ .with({ features: [FEATURES.COUNTERFACTUAL, FEATURES.MULTI_CHAIN_SAFE_CREATION] as any })
+ .with({ recommendedMasterCopyVersion: '1.3.0' })
.with({ l2: true })
.build(),
),
@@ -303,7 +302,8 @@ describe('create/logic', () => {
chainBuilder()
.with({ chainId: '137' })
// Multichain creation is toggled on
- .with({ features: [FEATURES.SAFE_141, FEATURES.COUNTERFACTUAL, FEATURES.MULTI_CHAIN_SAFE_CREATION] as any })
+ .with({ features: [FEATURES.COUNTERFACTUAL, FEATURES.MULTI_CHAIN_SAFE_CREATION] as any })
+ .with({ recommendedMasterCopyVersion: '1.4.1' })
.with({ l2: true })
.build(),
),
@@ -336,6 +336,7 @@ describe('create/logic', () => {
.with({ chainId: '324' })
// Multichain and 1.4.1 creation is toggled off
.with({ features: [FEATURES.COUNTERFACTUAL] as any })
+ .with({ recommendedMasterCopyVersion: '1.3.0' })
.with({ l2: true })
.build(),
),
diff --git a/src/components/settings/ContractVersion/index.tsx b/src/components/settings/ContractVersion/index.tsx
index 151548f3dc..63450e4dc1 100644
--- a/src/components/settings/ContractVersion/index.tsx
+++ b/src/components/settings/ContractVersion/index.tsx
@@ -57,12 +57,14 @@ export const ContractVersion = () => {
sx={{ mt: 2, borderRadius: '2px', borderColor: '#B0FFC9' }}
icon={}
>
- New version is available: {latestSafeVersion}
+
+ New version is available: {latestSafeVersion} (
+ changelog)
+
-
+
Update now to take advantage of new features and the highest security standards available. You will need to
- confirm this update just like any other transaction.{' '}
- GitHub
+ confirm this update just like any other transaction.
diff --git a/src/components/transactions/TxDetails/TxData/DecodedData/index.tsx b/src/components/transactions/TxDetails/TxData/DecodedData/index.tsx
index a67e483598..574a1eea1a 100644
--- a/src/components/transactions/TxDetails/TxData/DecodedData/index.tsx
+++ b/src/components/transactions/TxDetails/TxData/DecodedData/index.tsx
@@ -12,7 +12,6 @@ import MethodCall from './MethodCall'
import useSafeAddress from '@/hooks/useSafeAddress'
import { sameAddress } from '@/utils/addresses'
import { DelegateCallWarning } from '@/components/transactions/Warning'
-import { isMigrateToL2TxData } from '@/utils/transaction-guards'
interface Props {
txData: TransactionDetails['txData']
@@ -58,11 +57,9 @@ export const DecodedData = ({ txData, toInfo }: Props): ReactElement | null => {
decodedData =
}
- const isL2Migration = isMigrateToL2TxData(txData, chainInfo?.chainId)
-
return (
- {isDelegateCall && }
+ {isDelegateCall && }
{method ? (
diff --git a/src/components/transactions/TxDetails/TxData/MigrationToL2TxData/index.tsx b/src/components/transactions/TxDetails/TxData/MigrationToL2TxData/index.tsx
index 2d4a106297..835c9eb835 100644
--- a/src/components/transactions/TxDetails/TxData/MigrationToL2TxData/index.tsx
+++ b/src/components/transactions/TxDetails/TxData/MigrationToL2TxData/index.tsx
@@ -12,7 +12,7 @@ import { zeroPadValue } from 'ethers'
import DecodedData from '../DecodedData'
import ErrorMessage from '@/components/tx/ErrorMessage'
import { useSafeSDK } from '@/hooks/coreSDK/safeCoreSDK'
-import { MigrateToL2Information } from '@/components/tx/SignOrExecuteForm/MigrateToL2Information'
+import { MigrateToL2Information } from '@/components/tx/confirmation-views/MigrateToL2Information'
import { Box } from '@mui/material'
import { isMultisigDetailedExecutionInfo } from '@/utils/transaction-guards'
@@ -73,7 +73,8 @@ export const MigrationToL2TxData = ({ txDetails }: { txDetails: TransactionDetai
return (
-
+
+
{realSafeTxError ? (
{realSafeTxError.message}
) : decodedRealTxError ? (
diff --git a/src/components/transactions/TxDetails/TxData/SafeUpdate/index.tsx b/src/components/transactions/TxDetails/TxData/SafeUpdate/index.tsx
new file mode 100644
index 0000000000..8907df4b90
--- /dev/null
+++ b/src/components/transactions/TxDetails/TxData/SafeUpdate/index.tsx
@@ -0,0 +1,25 @@
+import { Box, Stack } from '@mui/material'
+import type { TransactionData } from '@safe-global/safe-gateway-typescript-sdk'
+import DecodedData from '../DecodedData'
+
+function SafeUpdate({ txData }: { txData?: TransactionData }) {
+ return (
+
+
+ Safe version update
+
+
+
+
+ )
+}
+
+export default SafeUpdate
diff --git a/src/components/transactions/TxDetails/TxData/index.tsx b/src/components/transactions/TxDetails/TxData/index.tsx
index 21ed9d064a..fc2bd276de 100644
--- a/src/components/transactions/TxDetails/TxData/index.tsx
+++ b/src/components/transactions/TxDetails/TxData/index.tsx
@@ -1,6 +1,11 @@
import SettingsChangeTxInfo from '@/components/transactions/TxDetails/TxData/SettingsChange'
import type { SpendingLimitMethods } from '@/utils/transaction-guards'
-import { isExecTxData, isOnChainConfirmationTxData, isStakingTxWithdrawInfo } from '@/utils/transaction-guards'
+import {
+ isExecTxData,
+ isOnChainConfirmationTxData,
+ isSafeUpdateTxData,
+ isStakingTxWithdrawInfo,
+} from '@/utils/transaction-guards'
import { isStakingTxExitInfo } from '@/utils/transaction-guards'
import {
isCancellationTxInfo,
@@ -28,6 +33,7 @@ import StakingTxExitDetails from '@/features/stake/components/StakingTxExitDetai
import StakingTxWithdrawDetails from '@/features/stake/components/StakingTxWithdrawDetails'
import { OnChainConfirmation } from './NestedTransaction/OnChainConfirmation'
import { ExecTransaction } from './NestedTransaction/ExecTransaction'
+import SafeUpdate from './SafeUpdate'
const TxData = ({
txDetails,
@@ -87,6 +93,10 @@ const TxData = ({
return
}
+ if (isSafeUpdateTxData(txDetails.txData)) {
+ return
+ }
+
return
}
diff --git a/src/components/tx-flow/SafeTxProvider.tsx b/src/components/tx-flow/SafeTxProvider.tsx
index e46ec0a846..63dd9d9a9d 100644
--- a/src/components/tx-flow/SafeTxProvider.tsx
+++ b/src/components/tx-flow/SafeTxProvider.tsx
@@ -7,7 +7,7 @@ import { Errors, logError } from '@/services/exceptions'
import type { EIP712TypedData } from '@safe-global/safe-gateway-typescript-sdk'
import useSafeInfo from '@/hooks/useSafeInfo'
import { useCurrentChain } from '@/hooks/useChains'
-import { prependSafeToL2Migration } from '@/utils/transactions'
+import { prependSafeToL2Migration } from '@/utils/safe-migrations'
import { useSelectAvailableSigner } from '@/hooks/wallets/useSelectAvailableSigner'
export type SafeTxContextParams = {
diff --git a/src/components/tx-flow/flows/UpdateSafe/UpdateSafeReview.tsx b/src/components/tx-flow/flows/UpdateSafe/UpdateSafeReview.tsx
index 4fe31a6fda..14e8652231 100644
--- a/src/components/tx-flow/flows/UpdateSafe/UpdateSafeReview.tsx
+++ b/src/components/tx-flow/flows/UpdateSafe/UpdateSafeReview.tsx
@@ -1,55 +1,25 @@
import { useContext } from 'react'
-import { Typography } from '@mui/material'
-
-import ExternalLink from '@/components/common/ExternalLink'
import { useCurrentChain } from '@/hooks/useChains'
import useSafeInfo from '@/hooks/useSafeInfo'
import { createUpdateSafeTxs } from '@/services/tx/safeUpdateParams'
-import { createMultiSendCallOnlyTx } from '@/services/tx/tx-sender'
+import { createMultiSendCallOnlyTx, createTx } from '@/services/tx/tx-sender'
import { SafeTxContext } from '../../SafeTxProvider'
import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm'
import useAsync from '@/hooks/useAsync'
-import { getLatestSafeVersion } from '@/utils/chains'
export const UpdateSafeReview = () => {
const { safe, safeLoaded } = useSafeInfo()
const chain = useCurrentChain()
const { setSafeTx, setSafeTxError } = useContext(SafeTxContext)
- const latestSafeVersion = getLatestSafeVersion(chain)
-
useAsync(async () => {
- if (!chain || !safeLoaded) {
- return
- }
+ if (!chain || !safeLoaded) return
const txs = await createUpdateSafeTxs(safe, chain)
- createMultiSendCallOnlyTx(txs).then(setSafeTx).catch(setSafeTxError)
- }, [safe, safeLoaded, chain, setSafeTx, setSafeTxError])
+ const safeTxPromise = txs.length > 1 ? createMultiSendCallOnlyTx(txs) : createTx(txs[0])
- return (
-
-
- Update now to take advantage of new features and the highest security standards available.
-
-
-
- To check details about updates added by this smart contract version please visit{' '}
-
- latest Safe Account contracts changelog
-
-
-
-
- You will need to confirm this update just like any other transaction. This means other signers will have to
- confirm the update in case more than one confirmation is required for this Safe Account.
-
+ safeTxPromise.then(setSafeTx).catch(setSafeTxError)
+ }, [safe, safeLoaded, chain, setSafeTx, setSafeTxError])
-
- Warning: this upgrade will invalidate all unexecuted transactions. This means you will be unable to
- access or execute them after the upgrade. Please make sure to execute any remaining transactions before
- upgrading.
-
-
- )
+ return
}
diff --git a/src/components/tx/SignOrExecuteForm/SignOrExecuteForm.tsx b/src/components/tx/SignOrExecuteForm/SignOrExecuteForm.tsx
index f5944f1a52..e7d4fe888a 100644
--- a/src/components/tx/SignOrExecuteForm/SignOrExecuteForm.tsx
+++ b/src/components/tx/SignOrExecuteForm/SignOrExecuteForm.tsx
@@ -19,28 +19,20 @@ import UnknownContractError from './UnknownContractError'
import { ErrorBoundary } from '@sentry/react'
import ApprovalEditor from '../ApprovalEditor'
import { isDelegateCall } from '@/services/tx/tx-sender/sdk'
-import { getTransactionTrackingType } from '@/services/analytics/tx-tracking'
-import { TX_EVENTS } from '@/services/analytics/events/transactions'
-import { trackEvent } from '@/services/analytics'
import useChainId from '@/hooks/useChainId'
import ExecuteThroughRoleForm from './ExecuteThroughRoleForm'
import { findAllowingRole, findMostLikelyRole, useRoles } from './ExecuteThroughRoleForm/hooks'
import useIsSafeOwner from '@/hooks/useIsSafeOwner'
import { BlockaidBalanceChanges } from '../security/blockaid/BlockaidBalanceChange'
import { Blockaid } from '../security/blockaid'
-
-import { MigrateToL2Information } from './MigrateToL2Information'
-import { extractMigrationL2MasterCopyAddress } from '@/utils/transactions'
-
import { useLazyGetTransactionDetailsQuery } from '@/store/api/gateway'
import { useApprovalInfos } from '../ApprovalEditor/hooks/useApprovalInfos'
-
import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'
import NetworkWarning from '@/components/new-safe/create/NetworkWarning'
import ConfirmationView from '../confirmation-views'
import { SignerForm } from './SignerForm'
import { useSigner } from '@/hooks/wallets/useWallet'
-import { isNestedConfirmationTxInfo } from '@/utils/transaction-guards'
+import { trackTxEvents } from './tracking'
export type SubmitCallback = (txId: string, isExecuted?: boolean) => void
@@ -58,76 +50,6 @@ export type SignOrExecuteProps = {
showMethodCall?: boolean
}
-const trackTxEvents = (
- details: TransactionDetails | undefined,
- isCreation: boolean,
- isExecuted: boolean,
- isRoleExecution: boolean,
- isProposerCreation: boolean,
- isParentSigner: boolean,
- origin?: string,
-) => {
- const isNestedConfirmation = !!details && isNestedConfirmationTxInfo(details.txInfo)
-
- const creationEvent = getCreationEvent({ isParentSigner, isRoleExecution, isProposerCreation })
- const confirmationEvent = getConfirmationEvent({ isParentSigner, isNestedConfirmation })
- const executionEvent = getExecutionEvent({ isParentSigner, isNestedConfirmation, isRoleExecution })
-
- const event = (() => {
- if (isCreation) {
- return creationEvent
- }
- if (isExecuted) {
- return executionEvent
- }
- return confirmationEvent
- })()
-
- const txType = getTransactionTrackingType(details, origin)
- trackEvent({ ...event, label: txType })
-
- // Immediate execution on creation
- if (isCreation && isExecuted) {
- trackEvent({ ...executionEvent, label: txType })
- }
-}
-
-function getCreationEvent(args: { isParentSigner: boolean; isRoleExecution: boolean; isProposerCreation: boolean }) {
- if (args.isParentSigner) {
- return TX_EVENTS.CREATE_VIA_PARENT
- }
- if (args.isRoleExecution) {
- return TX_EVENTS.CREATE_VIA_ROLE
- }
- if (args.isProposerCreation) {
- return TX_EVENTS.CREATE_VIA_PROPOSER
- }
- return TX_EVENTS.CREATE
-}
-
-function getConfirmationEvent(args: { isParentSigner: boolean; isNestedConfirmation: boolean }) {
- if (args.isParentSigner) {
- return TX_EVENTS.CONFIRM_VIA_PARENT
- }
- if (args.isNestedConfirmation) {
- return TX_EVENTS.CONFIRM_IN_PARENT
- }
- return TX_EVENTS.CONFIRM
-}
-
-function getExecutionEvent(args: { isParentSigner: boolean; isNestedConfirmation: boolean; isRoleExecution: boolean }) {
- if (args.isParentSigner) {
- return TX_EVENTS.EXECUTE_VIA_PARENT
- }
- if (args.isNestedConfirmation) {
- return TX_EVENTS.EXECUTE_IN_PARENT
- }
- if (args.isRoleExecution) {
- return TX_EVENTS.EXECUTE_VIA_ROLE
- }
- return TX_EVENTS.EXECUTE
-}
-
export const SignOrExecuteForm = ({
chainId,
safeTx,
@@ -157,8 +79,6 @@ export const SignOrExecuteForm = ({
const isProposer = useIsWalletProposer()
const isProposing = isProposer && !isSafeOwner && isCreation
const isCounterfactualSafe = !safe.deployed
- const multiChainMigrationTarget = extractMigrationL2MasterCopyAddress(safeTx)
- const isMultiChainMigration = !!multiChainMigrationTarget
// Check if a Zodiac Roles mod is enabled and if the user is a member of any role that allows the transaction
const roles = useRoles(
@@ -208,7 +128,6 @@ export const SignOrExecuteForm = ({
<>
{props.children}
- {isMultiChainMigration && }
- {!isMultiChainMigration && }
+
diff --git a/src/components/tx/SignOrExecuteForm/UnknownContractError.tsx b/src/components/tx/SignOrExecuteForm/UnknownContractError.tsx
index 931a7dd378..47b4de55cd 100644
--- a/src/components/tx/SignOrExecuteForm/UnknownContractError.tsx
+++ b/src/components/tx/SignOrExecuteForm/UnknownContractError.tsx
@@ -1,19 +1,24 @@
-import { type ReactElement } from 'react'
+import { useMemo, type ReactElement } from 'react'
+import type { TransactionData } from '@safe-global/safe-gateway-typescript-sdk'
import ExternalLink from '@/components/common/ExternalLink'
import { useCurrentChain } from '@/hooks/useChains'
import useSafeInfo from '@/hooks/useSafeInfo'
import { getExplorerLink } from '@/utils/gateway'
import ErrorMessage from '../ErrorMessage'
import { isValidMasterCopy } from '@/services/contracts/safeContracts'
+import { extractMigrationL2MasterCopyAddress } from '@/utils/safe-migrations'
-const UnknownContractError = (): ReactElement | null => {
+const UnknownContractError = ({ txData }: { txData: TransactionData | undefined }): ReactElement | null => {
const { safe, safeAddress } = useSafeInfo()
const currentChain = useCurrentChain()
+ const newMasterCopy = useMemo(() => {
+ return txData && extractMigrationL2MasterCopyAddress(txData)
+ }, [txData])
// Unsupported base contract
const isUnknown = !isValidMasterCopy(safe.implementationVersionState)
- if (!isUnknown) return null
+ if (!isUnknown || !newMasterCopy) return null
return (
diff --git a/src/components/tx/SignOrExecuteForm/tracking.ts b/src/components/tx/SignOrExecuteForm/tracking.ts
new file mode 100644
index 0000000000..50c6a6144c
--- /dev/null
+++ b/src/components/tx/SignOrExecuteForm/tracking.ts
@@ -0,0 +1,75 @@
+import { trackEvent } from '@/services/analytics'
+import { TX_EVENTS } from '@/services/analytics/events/transactions'
+import { getTransactionTrackingType } from '@/services/analytics/tx-tracking'
+import { isNestedConfirmationTxInfo } from '@/utils/transaction-guards'
+import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'
+
+function getCreationEvent(args: { isParentSigner: boolean; isRoleExecution: boolean; isProposerCreation: boolean }) {
+ if (args.isParentSigner) {
+ return TX_EVENTS.CREATE_VIA_PARENT
+ }
+ if (args.isRoleExecution) {
+ return TX_EVENTS.CREATE_VIA_ROLE
+ }
+ if (args.isProposerCreation) {
+ return TX_EVENTS.CREATE_VIA_PROPOSER
+ }
+ return TX_EVENTS.CREATE
+}
+
+function getConfirmationEvent(args: { isParentSigner: boolean; isNestedConfirmation: boolean }) {
+ if (args.isParentSigner) {
+ return TX_EVENTS.CONFIRM_VIA_PARENT
+ }
+ if (args.isNestedConfirmation) {
+ return TX_EVENTS.CONFIRM_IN_PARENT
+ }
+ return TX_EVENTS.CONFIRM
+}
+
+function getExecutionEvent(args: { isParentSigner: boolean; isNestedConfirmation: boolean; isRoleExecution: boolean }) {
+ if (args.isParentSigner) {
+ return TX_EVENTS.EXECUTE_VIA_PARENT
+ }
+ if (args.isNestedConfirmation) {
+ return TX_EVENTS.EXECUTE_IN_PARENT
+ }
+ if (args.isRoleExecution) {
+ return TX_EVENTS.EXECUTE_VIA_ROLE
+ }
+ return TX_EVENTS.EXECUTE
+}
+
+export function trackTxEvents(
+ details: TransactionDetails | undefined,
+ isCreation: boolean,
+ isExecuted: boolean,
+ isRoleExecution: boolean,
+ isProposerCreation: boolean,
+ isParentSigner: boolean,
+ origin?: string,
+) {
+ const isNestedConfirmation = !!details && isNestedConfirmationTxInfo(details.txInfo)
+
+ const creationEvent = getCreationEvent({ isParentSigner, isRoleExecution, isProposerCreation })
+ const confirmationEvent = getConfirmationEvent({ isParentSigner, isNestedConfirmation })
+ const executionEvent = getExecutionEvent({ isParentSigner, isNestedConfirmation, isRoleExecution })
+
+ const event = (() => {
+ if (isCreation) {
+ return creationEvent
+ }
+ if (isExecuted) {
+ return executionEvent
+ }
+ return confirmationEvent
+ })()
+
+ const txType = getTransactionTrackingType(details, origin)
+ trackEvent({ ...event, label: txType })
+
+ // Immediate execution on creation
+ if (isCreation && isExecuted) {
+ trackEvent({ ...executionEvent, label: txType })
+ }
+}
diff --git a/src/components/tx/SignOrExecuteForm/MigrateToL2Information.tsx b/src/components/tx/confirmation-views/MigrateToL2Information/index.tsx
similarity index 79%
rename from src/components/tx/SignOrExecuteForm/MigrateToL2Information.tsx
rename to src/components/tx/confirmation-views/MigrateToL2Information/index.tsx
index 6240d7b4de..431b862c83 100644
--- a/src/components/tx/SignOrExecuteForm/MigrateToL2Information.tsx
+++ b/src/components/tx/confirmation-views/MigrateToL2Information/index.tsx
@@ -1,14 +1,21 @@
+import { useMemo } from 'react'
import { Alert, AlertTitle, Box, SvgIcon, Typography } from '@mui/material'
+import type { TransactionData } from '@safe-global/safe-gateway-typescript-sdk'
import InfoOutlinedIcon from '@/public/images/notifications/info.svg'
import NamedAddressInfo from '@/components/common/NamedAddressInfo'
+import { extractMigrationL2MasterCopyAddress } from '@/utils/safe-migrations'
export const MigrateToL2Information = ({
variant,
- newMasterCopy,
+ txData,
}: {
variant: 'history' | 'queue'
- newMasterCopy?: string
+ txData?: TransactionData
}) => {
+ const newMasterCopy = useMemo(() => {
+ return txData && extractMigrationL2MasterCopyAddress(txData)
+ }, [txData])
+
return (
}>
diff --git a/src/components/tx/confirmation-views/UpdateSafe/index.tsx b/src/components/tx/confirmation-views/UpdateSafe/index.tsx
new file mode 100644
index 0000000000..6dec5f3949
--- /dev/null
+++ b/src/components/tx/confirmation-views/UpdateSafe/index.tsx
@@ -0,0 +1,60 @@
+import type { ReactNode } from 'react'
+import { Alert, AlertTitle, Box, Divider, Stack, Typography } from '@mui/material'
+import { LATEST_SAFE_VERSION } from '@/config/constants'
+import { useCurrentChain } from '@/hooks/useChains'
+import useSafeInfo from '@/hooks/useSafeInfo'
+import { useQueuedTxsLength } from '@/hooks/useTxQueue'
+import ExternalLink from '@/components/common/ExternalLink'
+import { maybePlural } from '@/utils/formatters'
+
+function BgBox({ children, light }: { children: ReactNode; light?: boolean }) {
+ return (
+
+ {children}
+
+ )
+}
+
+function UpdateSafe() {
+ const { safe } = useSafeInfo()
+ const chain = useCurrentChain()
+ const queueSize = useQueuedTxsLength()
+ const latestSafeVersion = chain?.recommendedMasterCopyVersion || LATEST_SAFE_VERSION
+
+ return (
+ <>
+
+ Current version: {safe.version}
+ →
+ New version: {latestSafeVersion}
+
+
+
+ Read about the updates in the new Safe contracts version in the{' '}
+
+ version {latestSafeVersion} changelog
+
+
+
+ {queueSize && (
+
+ This upgrade will invalidate all queued transactions!
+ You have {queueSize} unexecuted transaction{maybePlural(parseInt(queueSize))}. Please make sure to execute or
+ delete them before upgrading, otherwise you'll have to reject or replace them after the upgrade.
+
+ )}
+
+
+ >
+ )
+}
+
+export default UpdateSafe
diff --git a/src/components/tx/confirmation-views/index.tsx b/src/components/tx/confirmation-views/index.tsx
index 367b10123d..849ba5dab6 100644
--- a/src/components/tx/confirmation-views/index.tsx
+++ b/src/components/tx/confirmation-views/index.tsx
@@ -7,6 +7,8 @@ import {
isExecTxData,
isOnChainConfirmationTxData,
isOrderTxInfo,
+ isSafeToL2MigrationTxData,
+ isSafeUpdateTxData,
isSwapOrderTxInfo,
} from '@/utils/transaction-guards'
import { type ReactNode, useContext, useMemo } from 'react'
@@ -22,6 +24,8 @@ import { ExecTransaction } from '@/components/transactions/TxDetails/TxData/Nest
import { type ReactElement } from 'react'
import SwapOrder from './SwapOrder'
import StakingTx from './StakingTx'
+import UpdateSafe from './UpdateSafe'
+import { MigrateToL2Information } from './MigrateToL2Information'
type ConfirmationViewProps = {
txDetails?: TransactionDetails
@@ -55,6 +59,12 @@ const getConfirmationViewComponent = ({
if (isAnyStakingTxInfo(txInfo)) return
+ if (isCustomTxInfo(txInfo) && isSafeUpdateTxData(txDetails.txData)) return
+
+ if (isCustomTxInfo(txInfo) && isSafeToL2MigrationTxData(txDetails.txData)) {
+ return
+ }
+
return null
}
diff --git a/src/features/multichain/utils/utils.ts b/src/features/multichain/utils/utils.ts
index a9d549d50b..be50ff10f4 100644
--- a/src/features/multichain/utils/utils.ts
+++ b/src/features/multichain/utils/utils.ts
@@ -1,16 +1,17 @@
-import type { DecodedDataResponse } from '@safe-global/safe-gateway-typescript-sdk'
+import type { DecodedDataResponse, ChainInfo, SafeOverview } from '@safe-global/safe-gateway-typescript-sdk'
+import semverSatisfies from 'semver/functions/satisfies'
+import memoize from 'lodash/memoize'
+import { keccak256, ethers, solidityPacked, getCreate2Address, type Provider } from 'ethers'
-import { type ChainInfo, type SafeOverview } from '@safe-global/safe-gateway-typescript-sdk'
import { type UndeployedSafesState, type ReplayedSafeProps } from '@/store/slices'
import { sameAddress } from '@/utils/addresses'
import { Safe_proxy_factory__factory } from '@/types/contracts'
-import { keccak256, ethers, solidityPacked, getCreate2Address, type Provider } from 'ethers'
import { extractCounterfactualSafeSetup } from '@/features/counterfactual/utils'
import { encodeSafeSetupCall } from '@/components/new-safe/create/logic'
-import memoize from 'lodash/memoize'
import { FEATURES, hasFeature } from '@/utils/chains'
import { type SafeItem } from '@/features/myAccounts/hooks/useAllSafes'
import { type MultiChainSafeItem } from '@/features/myAccounts/hooks/useAllSafesGrouped'
+import { LATEST_SAFE_VERSION } from '@/config/constants'
type SafeSetup = {
owners: string[]
@@ -121,14 +122,14 @@ export const predictAddressBasedOnReplayData = async (safeCreationData: Replayed
}
export const hasMultiChainCreationFeatures = (chain: ChainInfo): boolean => {
- return (
- hasFeature(chain, FEATURES.MULTI_CHAIN_SAFE_CREATION) &&
- hasFeature(chain, FEATURES.COUNTERFACTUAL) &&
- hasFeature(chain, FEATURES.SAFE_141)
- )
+ return hasFeature(chain, FEATURES.MULTI_CHAIN_SAFE_CREATION) && hasFeature(chain, FEATURES.COUNTERFACTUAL)
}
export const hasMultiChainAddNetworkFeature = (chain: ChainInfo | undefined): boolean => {
if (!chain) return false
- return hasFeature(chain, FEATURES.MULTI_CHAIN_SAFE_ADD_NETWORK) && hasFeature(chain, FEATURES.COUNTERFACTUAL)
+ return (
+ hasFeature(chain, FEATURES.MULTI_CHAIN_SAFE_ADD_NETWORK) &&
+ hasFeature(chain, FEATURES.COUNTERFACTUAL) &&
+ semverSatisfies(chain.recommendedMasterCopyVersion || LATEST_SAFE_VERSION, '>=1.4.1')
+ )
}
diff --git a/src/features/recovery/services/__tests__/recovery-state.test.ts b/src/features/recovery/services/__tests__/recovery-state.test.ts
index 869a90d09f..793289834f 100644
--- a/src/features/recovery/services/__tests__/recovery-state.test.ts
+++ b/src/features/recovery/services/__tests__/recovery-state.test.ts
@@ -15,8 +15,7 @@ import { useWeb3ReadOnly } from '@/hooks/wallets/web3'
import { encodeMultiSendData } from '@safe-global/protocol-kit/dist/src/utils/transactions/utils'
import { getMultiSendCallOnlyDeployment, getSafeSingletonDeployment } from '@safe-global/safe-deployments'
import { Interface } from 'ethers'
-import { FEATURES, getLatestSafeVersion } from '@/utils/chains'
-import { type FEATURES as GatewayFeatures } from '@safe-global/safe-gateway-typescript-sdk'
+import { getLatestSafeVersion } from '@/utils/chains'
import { chainBuilder } from '@/tests/builders/chains'
jest.mock('@/hooks/wallets/web3')
@@ -24,9 +23,7 @@ jest.mock('@/hooks/wallets/web3')
const mockUseWeb3ReadOnly = useWeb3ReadOnly as jest.MockedFunction
const latestSafeVersion = getLatestSafeVersion(
- chainBuilder()
- .with({ chainId: '1', features: [FEATURES.SAFE_141 as unknown as GatewayFeatures] })
- .build(),
+ chainBuilder().with({ chainId: '1', recommendedMasterCopyVersion: '1.4.1' }).build(),
)
const PRE_MULTI_SEND_CALL_ONLY_VERSIONS = ['1.0.0', '1.1.1']
const SUPPORTED_MULTI_SEND_CALL_ONLY_VERSIONS = [
diff --git a/src/services/contracts/__tests__/deployments.test.ts b/src/services/contracts/__tests__/deployments.test.ts
index 209250ee4c..38912aa1c7 100644
--- a/src/services/contracts/__tests__/deployments.test.ts
+++ b/src/services/contracts/__tests__/deployments.test.ts
@@ -2,21 +2,17 @@ import * as safeDeployments from '@safe-global/safe-deployments'
import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'
import * as deployments from '../deployments'
-import { FEATURES, getLatestSafeVersion } from '@/utils/chains'
+import { getLatestSafeVersion } from '@/utils/chains'
import { chainBuilder } from '@/tests/builders/chains'
-const mainnetInfo = chainBuilder()
- .with({ chainId: '1', features: [FEATURES.SAFE_141 as any], l2: false })
- .build()
-const l2ChainInfo = chainBuilder()
- .with({ chainId: '137', features: [FEATURES.SAFE_141 as any], l2: true })
- .build()
+const mainnetInfo = chainBuilder().with({ chainId: '1', l2: false, recommendedMasterCopyVersion: '1.4.1' }).build()
+const l2ChainInfo = chainBuilder().with({ chainId: '137', l2: true, recommendedMasterCopyVersion: '1.4.1' }).build()
const unsupportedChainInfo = chainBuilder()
- .with({ chainId: '69420', features: [FEATURES.SAFE_141 as any], l2: false })
+ .with({ chainId: '69420', l2: false, recommendedMasterCopyVersion: '1.3.0' })
.build()
const unsupportedL2ChainInfo = chainBuilder()
- .with({ chainId: '69420', features: [FEATURES.SAFE_141 as any], l2: true })
+ .with({ chainId: '69420', l2: true, recommendedMasterCopyVersion: '1.3.0' })
.build()
const latestSafeVersion = getLatestSafeVersion(mainnetInfo)
diff --git a/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx b/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx
index 65264bc24f..1ac75f5bb2 100644
--- a/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx
+++ b/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx
@@ -17,7 +17,6 @@ import { Interface } from 'ethers'
import { getCreateCallDeployment } from '@safe-global/safe-deployments'
import { useCurrentChain } from '@/hooks/useChains'
import { chainBuilder } from '@/tests/builders/chains'
-import { FEATURES } from '@/utils/chains'
const appInfo = {
id: 1,
@@ -47,9 +46,7 @@ describe('useSafeWalletProvider', () => {
jest.clearAllMocks()
mockUseCurrentChain.mockReturnValue(
- chainBuilder()
- .with({ chainId: '1', features: [FEATURES.SAFE_141 as any] })
- .build(),
+ chainBuilder().with({ chainId: '1', recommendedMasterCopyVersion: '1.4.1' }).build(),
)
})
diff --git a/src/services/tx/__tests__/safeUpdateParams.test.ts b/src/services/tx/__tests__/safeUpdateParams.test.ts
index d3e74623e2..314f9cfd5a 100644
--- a/src/services/tx/__tests__/safeUpdateParams.test.ts
+++ b/src/services/tx/__tests__/safeUpdateParams.test.ts
@@ -10,7 +10,7 @@ import { type SafeInfo } from '@safe-global/safe-gateway-typescript-sdk'
import { Interface, JsonRpcProvider } from 'ethers'
import { createUpdateSafeTxs } from '../safeUpdateParams'
import * as web3 from '@/hooks/wallets/web3'
-import { FEATURES, getLatestSafeVersion } from '@/utils/chains'
+import { getLatestSafeVersion } from '@/utils/chains'
import { chainBuilder } from '@/tests/builders/chains'
const MOCK_SAFE_ADDRESS = '0x0000000000000000000000000000000000005AFE'
@@ -39,7 +39,7 @@ describe('safeUpgradeParams', () => {
} as SafeInfo
const mockChainInfo = chainBuilder()
- .with({ chainId: '1', l2: false, features: [FEATURES.SAFE_141 as any] })
+ .with({ chainId: '1', l2: false, recommendedMasterCopyVersion: '1.4.1' })
.build()
const txs = await createUpdateSafeTxs(mockSafe, mockChainInfo)
const [masterCopyTx, fallbackHandlerTx] = txs
@@ -74,7 +74,7 @@ describe('safeUpgradeParams', () => {
version: '1.1.1',
} as SafeInfo
const mockChainInfo = chainBuilder()
- .with({ chainId: '1', l2: false, features: [FEATURES.SAFE_141 as any] })
+ .with({ chainId: '1', l2: false, recommendedMasterCopyVersion: '1.4.1' })
.build()
const txs = await createUpdateSafeTxs(mockSafe, mockChainInfo)
const [masterCopyTx, fallbackHandlerTx] = txs
@@ -111,7 +111,7 @@ describe('safeUpgradeParams', () => {
version: '1.1.1',
} as SafeInfo
const mockChainInfo = chainBuilder()
- .with({ chainId: '100', l2: true, features: [FEATURES.SAFE_141 as any] })
+ .with({ chainId: '100', l2: true, recommendedMasterCopyVersion: '1.4.1' })
.build()
const txs = await createUpdateSafeTxs(mockSafe, mockChainInfo)
diff --git a/src/services/tx/safeUpdateParams.ts b/src/services/tx/safeUpdateParams.ts
index 9b0fc5f320..36fc3b7ef1 100644
--- a/src/services/tx/safeUpdateParams.ts
+++ b/src/services/tx/safeUpdateParams.ts
@@ -2,11 +2,13 @@ import type { SafeContractImplementationType } from '@safe-global/protocol-kit/d
import type { MetaTransactionData } from '@safe-global/safe-core-sdk-types'
import { OperationType } from '@safe-global/safe-core-sdk-types'
import type { ChainInfo, SafeInfo } from '@safe-global/safe-gateway-typescript-sdk'
+import semverSatisfies from 'semver/functions/satisfies'
import { getReadOnlyFallbackHandlerContract, getReadOnlyGnosisSafeContract } from '@/services/contracts/safeContracts'
import { assertValidSafeVersion } from '@/hooks/coreSDK/safeCoreSDK'
import { SAFE_FEATURES } from '@safe-global/protocol-kit/dist/src/utils/safeVersions'
import { hasSafeFeature } from '@/utils/safe-versions'
import { getLatestSafeVersion } from '@/utils/chains'
+import { createUpdateMigration } from '@/utils/safe-migrations'
const getChangeFallbackHandlerCallData = async (
safeContractInstance: SafeContractImplementationType,
@@ -24,14 +26,21 @@ const getChangeFallbackHandlerCallData = async (
}
/**
- * Creates two transactions:
+ * For 1.3.0 Safes, does a delegate call to a migration contract.
+ *
+ * For older Safes, creates two transactions:
* - change the mastercopy address
* - set the fallback handler address
- * Only works for safes < 1.3.0 as the changeMasterCopy function was removed
*/
export const createUpdateSafeTxs = async (safe: SafeInfo, chain: ChainInfo): Promise => {
assertValidSafeVersion(safe.version)
+ // 1.3.0 Safes are updated using a delegate call to a migration contract
+ if (semverSatisfies(safe.version, '1.3.0')) {
+ return [createUpdateMigration(chain)]
+ }
+
+ // For older Safes, we need to create two transactions
const latestMasterCopyAddress = await (
await getReadOnlyGnosisSafeContract(chain, getLatestSafeVersion(chain))
).getAddress()
diff --git a/src/tests/builders/chains.ts b/src/tests/builders/chains.ts
index 0f8f033340..ed3b333f0e 100644
--- a/src/tests/builders/chains.ts
+++ b/src/tests/builders/chains.ts
@@ -110,6 +110,7 @@ export const chainBuilder = (): IBuilder => {
disabledWallets: generateRandomArray(() => faker.word.sample(), { min: 1, max: 10 }),
// @ts-expect-error - we are using a local FEATURES enum
features: generateRandomArray(() => faker.helpers.enumValue(FEATURES), { min: 1, max: 10 }),
+ recommendedMasterCopyVersion: faker.system.semver(),
})
}
diff --git a/src/tests/mocks/chains.ts b/src/tests/mocks/chains.ts
index 8d9c6c346b..b194c3cd7b 100644
--- a/src/tests/mocks/chains.ts
+++ b/src/tests/mocks/chains.ts
@@ -68,6 +68,7 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [
chainName: null,
enabled: false,
},
+ recommendedMasterCopyVersion: '1.4.1',
},
{
transactionService: 'https://safe-transaction.xdai.gnosis.io',
@@ -122,6 +123,7 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [
chainName: null,
enabled: false,
},
+ recommendedMasterCopyVersion: '1.4.1',
},
{
transactionService: 'https://safe-transaction.polygon.gnosis.io',
@@ -182,6 +184,7 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [
chainName: null,
enabled: false,
},
+ recommendedMasterCopyVersion: '1.4.1',
},
{
transactionService: 'https://safe-transaction.bsc.gnosis.io',
@@ -238,6 +241,7 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [
chainName: null,
enabled: false,
},
+ recommendedMasterCopyVersion: '1.4.1',
},
{
transactionService: 'https://safe-transaction.ewc.gnosis.io',
@@ -292,6 +296,7 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [
chainName: null,
enabled: false,
},
+ recommendedMasterCopyVersion: '1.4.1',
},
{
transactionService: 'https://safe-transaction.arbitrum.gnosis.io',
@@ -339,6 +344,7 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [
chainName: null,
enabled: false,
},
+ recommendedMasterCopyVersion: '1.4.1',
},
{
transactionService: 'https://safe-transaction.aurora.gnosis.io',
@@ -387,6 +393,7 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [
chainName: null,
enabled: false,
},
+ recommendedMasterCopyVersion: '1.4.1',
},
{
transactionService: 'https://safe-transaction.avalanche.gnosis.io',
@@ -446,6 +453,7 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [
chainName: null,
enabled: false,
},
+ recommendedMasterCopyVersion: '1.4.1',
},
{
transactionService: 'https://safe-transaction.optimism.gnosis.io',
@@ -493,6 +501,7 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [
chainName: null,
enabled: false,
},
+ recommendedMasterCopyVersion: '1.4.1',
},
{
transactionService: 'https://safe-transaction.goerli.gnosis.io/',
@@ -548,6 +557,7 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [
chainName: null,
enabled: false,
},
+ recommendedMasterCopyVersion: '1.4.1',
},
{
transactionService: 'https://safe-transaction.rinkeby.gnosis.io',
@@ -593,6 +603,7 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [
chainName: null,
enabled: false,
},
+ recommendedMasterCopyVersion: '1.4.1',
},
{
transactionService: 'https://safe-transaction.volta.gnosis.io',
@@ -647,6 +658,7 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [
chainName: null,
enabled: false,
},
+ recommendedMasterCopyVersion: '1.4.1',
},
]
diff --git a/src/utils/__tests__/chains.test.ts b/src/utils/__tests__/chains.test.ts
index baebc5009f..38ec32d031 100644
--- a/src/utils/__tests__/chains.test.ts
+++ b/src/utils/__tests__/chains.test.ts
@@ -41,46 +41,20 @@ describe('chains', () => {
describe('chains', () => {
describe('getLatestSafeVersion', () => {
- it('should return 1.4.1 on supported networks', () => {
+ it('should return the version from recommendedMasterCopyVersion', () => {
expect(
- getLatestSafeVersion(
- chainBuilder()
- .with({ chainId: '1', features: [FEATURES.SAFE_141 as any] })
- .build(),
- ),
- ).toEqual('1.4.1')
- expect(
- getLatestSafeVersion(
- chainBuilder()
- .with({ chainId: '137', features: [FEATURES.SAFE_141 as any] })
- .build(),
- ),
+ getLatestSafeVersion(chainBuilder().with({ chainId: '1', recommendedMasterCopyVersion: '1.4.1' }).build()),
).toEqual('1.4.1')
expect(
- getLatestSafeVersion(
- chainBuilder()
- .with({ chainId: '11155111', features: [FEATURES.SAFE_141 as any] })
- .build(),
- ),
- ).toEqual('1.4.1')
+ getLatestSafeVersion(chainBuilder().with({ chainId: '137', recommendedMasterCopyVersion: '1.3.0' }).build()),
+ ).toEqual('1.3.0')
})
-
- it('should return 1.3.0 on networks where 1.4.1 is not released', () => {
+ it('should fall back to LATEST_VERSION', () => {
expect(
getLatestSafeVersion(
- chainBuilder()
- .with({ chainId: '324', features: [FEATURES.SAFE_141 as any] })
- .build(),
+ chainBuilder().with({ chainId: '11155111', recommendedMasterCopyVersion: null }).build(),
),
- ).toEqual('1.3.0')
- })
-
- it('should return 1.3.0 if the feature is off', () => {
- expect(getLatestSafeVersion(chainBuilder().with({ chainId: '1', features: [] }).build())).toEqual('1.3.0')
- expect(getLatestSafeVersion(chainBuilder().with({ chainId: '137', features: [] }).build())).toEqual('1.3.0')
- expect(getLatestSafeVersion(chainBuilder().with({ chainId: '11155111', features: [] }).build())).toEqual(
- '1.3.0',
- )
+ ).toEqual('1.4.1')
})
})
})
diff --git a/src/utils/__tests__/safe-migrations.test.ts b/src/utils/__tests__/safe-migrations.test.ts
new file mode 100644
index 0000000000..6ebdc3330b
--- /dev/null
+++ b/src/utils/__tests__/safe-migrations.test.ts
@@ -0,0 +1,353 @@
+import {
+ type ChainInfo,
+ ImplementationVersionState,
+ type TransactionData,
+} from '@safe-global/safe-gateway-typescript-sdk'
+import { OperationType } from '@safe-global/safe-core-sdk-types'
+import { extractMigrationL2MasterCopyAddress, prependSafeToL2Migration } from '../safe-migrations'
+import { extendedSafeInfoBuilder } from '@/tests/builders/safe'
+import { chainBuilder } from '@/tests/builders/chains'
+import { safeSignatureBuilder, safeTxBuilder, safeTxDataBuilder } from '@/tests/builders/safeTx'
+import {
+ getMultiSendCallOnlyDeployment,
+ getMultiSendDeployment,
+ getSafeL2SingletonDeployment,
+ getSafeSingletonDeployment,
+ getSafeToL2MigrationDeployment,
+} from '@safe-global/safe-deployments'
+import type Safe from '@safe-global/protocol-kit'
+import { encodeMultiSendData } from '@safe-global/protocol-kit'
+import { Multi_send__factory, Safe_to_l2_migration__factory } from '@/types/contracts'
+import { faker } from '@faker-js/faker'
+import { getAndValidateSafeSDK } from '@/services/tx/tx-sender/sdk'
+import { decodeMultiSendData } from '@safe-global/protocol-kit/dist/src/utils'
+import { checksumAddress } from '../addresses'
+import { createUpdateMigration } from '../safe-migrations'
+
+jest.mock('@/services/tx/tx-sender/sdk')
+
+const safeToL2MigrationDeployment = getSafeToL2MigrationDeployment()
+const safeToL2MigrationAddress = safeToL2MigrationDeployment?.defaultAddress
+const safeToL2MigrationInterface = Safe_to_l2_migration__factory.createInterface()
+const multisendInterface = Multi_send__factory.createInterface()
+
+describe('prependSafeToL2Migration', () => {
+ const mockGetAndValidateSdk = getAndValidateSafeSDK as jest.MockedFunction
+
+ beforeEach(() => {
+ // Mock create Tx
+ mockGetAndValidateSdk.mockReturnValue({
+ createTransaction: ({ transactions, onlyCalls }) => {
+ return Promise.resolve(
+ safeTxBuilder()
+ .with({
+ data: safeTxDataBuilder()
+ .with({
+ to: onlyCalls
+ ? (getMultiSendCallOnlyDeployment()?.defaultAddress ?? faker.finance.ethereumAddress())
+ : (getMultiSendDeployment()?.defaultAddress ?? faker.finance.ethereumAddress()),
+ value: '0',
+ data: Multi_send__factory.createInterface().encodeFunctionData('multiSend', [
+ encodeMultiSendData(transactions),
+ ]),
+ nonce: 0,
+ operation: 1,
+ })
+ .build(),
+ })
+ .build(),
+ )
+ },
+ } as Safe)
+ })
+
+ it('should return undefined for undefined safeTx', () => {
+ expect(
+ prependSafeToL2Migration(undefined, extendedSafeInfoBuilder().build(), chainBuilder().build()),
+ ).resolves.toBeUndefined()
+ })
+
+ it('should throw if chain is undefined', () => {
+ expect(() => prependSafeToL2Migration(undefined, extendedSafeInfoBuilder().build(), undefined)).toThrowError()
+ })
+
+ it('should not modify tx if the chain is L1', () => {
+ const safeTx = safeTxBuilder()
+ .with({ data: safeTxDataBuilder().with({ nonce: 0 }).build() })
+ .build()
+
+ const safeInfo = extendedSafeInfoBuilder()
+ .with({ implementationVersionState: ImplementationVersionState.UNKNOWN })
+ .build()
+
+ expect(prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: false }).build())).resolves.toEqual(
+ safeTx,
+ )
+ })
+
+ it('should not modify tx if the nonce is > 0', () => {
+ const safeTx = safeTxBuilder()
+ .with({ data: safeTxDataBuilder().with({ nonce: 1 }).build() })
+ .build()
+
+ const safeInfo = extendedSafeInfoBuilder()
+ .with({ implementationVersionState: ImplementationVersionState.UNKNOWN })
+ .build()
+
+ expect(prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true }).build())).resolves.toEqual(
+ safeTx,
+ )
+ })
+
+ it('should not modify tx if implementationState is correct', () => {
+ const safeTx = safeTxBuilder()
+ .with({ data: safeTxDataBuilder().with({ nonce: 0 }).build() })
+ .build()
+
+ const safeInfo = extendedSafeInfoBuilder()
+ .with({ implementationVersionState: ImplementationVersionState.UP_TO_DATE })
+ .build()
+ expect(prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true }).build())).resolves.toEqual(
+ safeTx,
+ )
+ })
+
+ it('should not modify tx if the tx is already signed', () => {
+ const safeTx = safeTxBuilder()
+ .with({ data: safeTxDataBuilder().with({ nonce: 0 }).build() })
+ .build()
+
+ safeTx.addSignature(safeSignatureBuilder().build())
+
+ const safeInfo = extendedSafeInfoBuilder()
+ .with({ implementationVersionState: ImplementationVersionState.UNKNOWN })
+ .build()
+
+ expect(prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true }).build())).resolves.toEqual(
+ safeTx,
+ )
+ })
+
+ it('should not modify tx if the chain has no migration lib deployed', () => {
+ const safeTx = safeTxBuilder()
+ .with({ data: safeTxDataBuilder().with({ nonce: 0 }).build() })
+ .build()
+
+ const safeInfo = extendedSafeInfoBuilder()
+ .with({ implementationVersionState: ImplementationVersionState.UNKNOWN })
+ .build()
+
+ expect(
+ prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true, chainId: '69420' }).build()),
+ ).resolves.toEqual(safeTx)
+ })
+
+ it('should not modify tx if the tx already migrates', () => {
+ const safeL2SingletonDeployment = getSafeL2SingletonDeployment()?.defaultAddress
+
+ const safeTx = safeTxBuilder()
+ .with({
+ data: safeTxDataBuilder()
+ .with({
+ nonce: 0,
+ to: safeToL2MigrationAddress,
+ data:
+ safeL2SingletonDeployment &&
+ safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [safeL2SingletonDeployment]),
+ })
+ .build(),
+ })
+ .build()
+ const safeInfo = extendedSafeInfoBuilder()
+ .with({
+ implementationVersionState: ImplementationVersionState.UNKNOWN,
+ implementation: {
+ name: '1.3.0',
+ value: getSafeSingletonDeployment()?.defaultAddress ?? faker.finance.ethereumAddress(),
+ },
+ })
+ .build()
+ expect(
+ prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true, chainId: '10' }).build()),
+ ).resolves.toEqual(safeTx)
+ const multiSendSafeTx = safeTxBuilder()
+ .with({
+ data: safeTxDataBuilder()
+ .with({
+ nonce: 0,
+ to: getMultiSendDeployment()?.defaultAddress,
+ data:
+ safeToL2MigrationAddress &&
+ safeL2SingletonDeployment &&
+ Multi_send__factory.createInterface().encodeFunctionData('multiSend', [
+ encodeMultiSendData([
+ {
+ value: '0',
+ operation: 1,
+ to: safeToL2MigrationAddress,
+ data: safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [safeL2SingletonDeployment]),
+ },
+ ]),
+ ]),
+ })
+ .build(),
+ })
+ .build()
+ expect(
+ prependSafeToL2Migration(multiSendSafeTx, safeInfo, chainBuilder().with({ l2: true, chainId: '10' }).build()),
+ ).resolves.toEqual(multiSendSafeTx)
+ })
+
+ it('should modify single txs if applicable', async () => {
+ const safeTx = safeTxBuilder()
+ .with({
+ data: safeTxDataBuilder()
+ .with({
+ nonce: 0,
+ to: faker.finance.ethereumAddress(),
+ data: faker.string.hexadecimal({ length: 10 }),
+ value: '0',
+ })
+ .build(),
+ })
+ .build()
+
+ const safeInfo = extendedSafeInfoBuilder()
+ .with({
+ implementationVersionState: ImplementationVersionState.UNKNOWN,
+ implementation: {
+ name: '1.3.0',
+ value: getSafeSingletonDeployment()?.defaultAddress ?? faker.finance.ethereumAddress(),
+ },
+ })
+ .build()
+
+ const modifiedTx = await prependSafeToL2Migration(
+ safeTx,
+ safeInfo,
+ chainBuilder().with({ l2: true, chainId: '10' }).build(),
+ )
+
+ expect(modifiedTx).not.toEqual(safeTx)
+ expect(modifiedTx?.data.to).toEqual(getMultiSendDeployment()?.defaultAddress)
+ const decodedMultiSend = decodeMultiSendData(modifiedTx!.data.data)
+ expect(decodedMultiSend).toHaveLength(2)
+ const safeL2SingletonDeployment = getSafeL2SingletonDeployment()?.defaultAddress
+
+ expect(decodedMultiSend).toEqual([
+ {
+ to: safeToL2MigrationAddress,
+ value: '0',
+ operation: 1,
+ data:
+ safeL2SingletonDeployment &&
+ safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [safeL2SingletonDeployment]),
+ },
+ {
+ to: checksumAddress(safeTx.data.to),
+ value: safeTx.data.value,
+ operation: safeTx.data.operation,
+ data: safeTx.data.data.toLowerCase(),
+ },
+ ])
+ })
+})
+
+describe('extractMigrationL2MasterCopyAddress', () => {
+ it('should return undefined for non multisend safeTx', () => {
+ expect(
+ extractMigrationL2MasterCopyAddress({
+ hexData:
+ '0xf8dc5dd9000000000000000000000000000000000000000000000000000000000000000100000000000000000000000065f8236309e5a99ff0d129d04e486ebce20dc7b00000000000000000000000000000000000000000000000000000000000000001',
+ } as TransactionData),
+ ).toBeUndefined()
+ })
+
+ it('should return undefined for multisend without migration', () => {
+ expect(
+ extractMigrationL2MasterCopyAddress({
+ hexData: multisendInterface.encodeFunctionData('multiSend', [
+ encodeMultiSendData([
+ {
+ to: faker.finance.ethereumAddress(),
+ data: faker.string.hexadecimal({ length: 64 }),
+ value: '0',
+ operation: 0,
+ },
+ {
+ to: faker.finance.ethereumAddress(),
+ data: faker.string.hexadecimal({ length: 64 }),
+ value: '0',
+ operation: 0,
+ },
+ ]),
+ ]),
+ } as TransactionData),
+ ).toBeUndefined()
+ })
+
+ it('should return migration address for multisend with migration as first tx', () => {
+ const l2SingletonAddress = getSafeL2SingletonDeployment()?.defaultAddress!
+ expect(
+ extractMigrationL2MasterCopyAddress({
+ hexData: multisendInterface.encodeFunctionData('multiSend', [
+ encodeMultiSendData([
+ {
+ to: safeToL2MigrationAddress!,
+ data: safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [l2SingletonAddress]),
+ value: '0',
+ operation: 1,
+ },
+ {
+ to: faker.finance.ethereumAddress(),
+ data: faker.string.hexadecimal({ length: 64 }),
+ value: '0',
+ operation: 0,
+ },
+ ]),
+ ]),
+ } as TransactionData),
+ ).toEqual(l2SingletonAddress)
+ })
+
+ describe('createUpdateMigration', () => {
+ const mockChain = {
+ chainId: '1',
+ l2: false,
+ recommendedMasterCopyVersion: '1.4.1',
+ } as unknown as ChainInfo
+
+ const mockChainOld = {
+ chainId: '1',
+ l2: false,
+ recommendedMasterCopyVersion: '1.3.0',
+ } as unknown as ChainInfo
+
+ it('should create a migration transaction for L1 chain', () => {
+ const result = createUpdateMigration(mockChain)
+
+ expect(result).toEqual({
+ operation: OperationType.DelegateCall,
+ data: '0xed007fc6',
+ to: '0x526643F69b81B008F46d95CD5ced5eC0edFFDaC6',
+ value: '0',
+ })
+ })
+
+ it('should create a migration transaction for L2 chain', () => {
+ const l2Chain = { ...mockChain, l2: true }
+ const result = createUpdateMigration(l2Chain)
+
+ expect(result).toEqual({
+ operation: OperationType.DelegateCall,
+ data: '0x68cb3d94',
+ to: '0x526643F69b81B008F46d95CD5ced5eC0edFFDaC6',
+ value: '0',
+ })
+ })
+
+ it('should throw an error if deployment is not found', () => {
+ expect(() => createUpdateMigration(mockChainOld)).toThrow('Migration deployment not found')
+ })
+ })
+})
diff --git a/src/utils/__tests__/transaction-guards.test.ts b/src/utils/__tests__/transaction-guards.test.ts
index d5d6453614..3443e57019 100644
--- a/src/utils/__tests__/transaction-guards.test.ts
+++ b/src/utils/__tests__/transaction-guards.test.ts
@@ -3,6 +3,7 @@ import {
isExecTxInfo,
isOnChainConfirmationTxData,
isOnChainConfirmationTxInfo,
+ isSafeUpdateTxData,
} from '../transaction-guards'
import { faker } from '@faker-js/faker'
import { Safe__factory } from '@/types/contracts'
@@ -164,4 +165,117 @@ describe('transaction-guards', () => {
).toBeTruthy()
})
})
+
+ describe('isSafeUpdateTxData', () => {
+ it('should return true for 1.3.0+ migrations', () => {
+ const mockTxData = {
+ hexData: '0xed007fc6',
+ to: {
+ value: '0x526643F69b81B008F46d95CD5ced5eC0edFFDaC6',
+ name: 'SafeMigration 1.4.1',
+ logoUri: '',
+ },
+ value: '0',
+ operation: 1,
+ trustedDelegateCallTarget: true,
+ }
+ expect(isSafeUpdateTxData(mockTxData)).toBeTruthy()
+ })
+
+ it('should return false for arbitrary txData', () => {
+ expect(
+ isSafeUpdateTxData({
+ operation: 0,
+ to: { value: faker.finance.ethereumAddress() },
+ trustedDelegateCallTarget: false,
+ addressInfoIndex: undefined,
+ dataDecoded: undefined,
+ hexData: faker.string.hexadecimal({ length: 64 }),
+ }),
+ ).toBeFalsy()
+ })
+
+ it('should return true for older Safe masterCopyChange calls', () => {
+ const mockTxData = {
+ hexData:
+ '0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f20085c9f5aa0f82a531087a356a55623cf05e7bb895000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247de7edef00000000000000000000000041675c099f32341bf84bfc5382af534df5c7461a0085c9f5aa0f82a531087a356a55623cf05e7bb89500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f08a0323000000000000000000000000fd0732dc9e303f09fcef3a7388ad10a83459ec990000000000000000000000000000',
+ dataDecoded: {
+ method: 'multiSend',
+ parameters: [
+ {
+ name: 'transactions',
+ type: 'bytes',
+ value:
+ '0x0085c9f5aa0f82a531087a356a55623cf05e7bb895000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247de7edef00000000000000000000000041675c099f32341bf84bfc5382af534df5c7461a0085c9f5aa0f82a531087a356a55623cf05e7bb89500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f08a0323000000000000000000000000fd0732dc9e303f09fcef3a7388ad10a83459ec99',
+ valueDecoded: [
+ {
+ operation: 0,
+ to: '0x85C9f5aA0F82A531087a356a55623Cf05E7Bb895',
+ value: '0',
+ data: '0x7de7edef00000000000000000000000041675c099f32341bf84bfc5382af534df5c7461a',
+ dataDecoded: {
+ method: 'changeMasterCopy',
+ parameters: [
+ {
+ name: '_masterCopy',
+ type: 'address',
+ value: '0x41675C099F32341bf84BFc5382aF534df5C7461a',
+ },
+ ],
+ },
+ },
+ {
+ operation: 0,
+ to: '0x85C9f5aA0F82A531087a356a55623Cf05E7Bb895',
+ value: '0',
+ data: '0xf08a0323000000000000000000000000fd0732dc9e303f09fcef3a7388ad10a83459ec99',
+ dataDecoded: {
+ method: 'setFallbackHandler',
+ parameters: [
+ {
+ name: 'handler',
+ type: 'address',
+ value: '0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ to: {
+ value: '0x40A2aCCbd92BCA938b02010E17A5b8929b49130D',
+ name: 'Safe: MultiSendCallOnly 1.3.0',
+ logoUri: '',
+ },
+ value: '0',
+ operation: 1,
+ trustedDelegateCallTarget: true,
+ }
+
+ expect(isSafeUpdateTxData(mockTxData)).toBeTruthy()
+ })
+
+ it('should return false for arbitrary multisends', () => {
+ const mockTxData = {
+ hexData:
+ '0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f20085c9f5aa0f82a531087a356a55623cf05e7bb895000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247de7edef00000000000000000000000041675c099f32341bf84bfc5382af534df5c7461a0085c9f5aa0f82a531087a356a55623cf05e7bb89500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f08a0323000000000000000000000000fd0732dc9e303f09fcef3a7388ad10a83459ec990000000000000000000000000000',
+ dataDecoded: {
+ method: 'multiSend',
+ parameters: [],
+ },
+ to: {
+ value: '0x40A2aCCbd92BCA938b02010E17A5b8929b49130D',
+ name: 'Safe: MultiSendCallOnly 1.3.0',
+ logoUri: '',
+ },
+ value: '0',
+ operation: 1,
+ trustedDelegateCallTarget: true,
+ }
+
+ expect(isSafeUpdateTxData(mockTxData)).toBeFalsy()
+ })
+ })
})
diff --git a/src/utils/__tests__/transactions.test.ts b/src/utils/__tests__/transactions.test.ts
index fb96d9c00a..b2c47a7083 100644
--- a/src/utils/__tests__/transactions.test.ts
+++ b/src/utils/__tests__/transactions.test.ts
@@ -5,43 +5,13 @@ import type {
SafeAppData,
Transaction,
} from '@safe-global/safe-gateway-typescript-sdk'
-import { TransactionInfoType, ImplementationVersionState } from '@safe-global/safe-gateway-typescript-sdk'
+import { TransactionInfoType } from '@safe-global/safe-gateway-typescript-sdk'
import { isMultiSendTxInfo } from '../transaction-guards'
-import {
- extractMigrationL2MasterCopyAddress,
- getQueuedTransactionCount,
- getTxOrigin,
- prependSafeToL2Migration,
-} from '../transactions'
-import { extendedSafeInfoBuilder } from '@/tests/builders/safe'
-import { chainBuilder } from '@/tests/builders/chains'
-import { safeSignatureBuilder, safeTxBuilder, safeTxDataBuilder } from '@/tests/builders/safeTx'
-import {
- getMultiSendCallOnlyDeployment,
- getMultiSendDeployment,
- getSafeL2SingletonDeployment,
- getSafeSingletonDeployment,
- getSafeToL2MigrationDeployment,
-} from '@safe-global/safe-deployments'
-import type Safe from '@safe-global/protocol-kit'
-import { encodeMultiSendData } from '@safe-global/protocol-kit'
-import { Multi_send__factory, Safe_to_l2_migration__factory } from '@/types/contracts'
-import { faker } from '@faker-js/faker'
-import { getAndValidateSafeSDK } from '@/services/tx/tx-sender/sdk'
-import { decodeMultiSendData } from '@safe-global/protocol-kit/dist/src/utils'
-import { checksumAddress } from '../addresses'
+import { getQueuedTransactionCount, getTxOrigin } from '../transactions'
jest.mock('@/services/tx/tx-sender/sdk')
-const safeToL2MigrationDeployment = getSafeToL2MigrationDeployment()
-const safeToL2MigrationAddress = safeToL2MigrationDeployment?.defaultAddress
-const safeToL2MigrationInterface = Safe_to_l2_migration__factory.createInterface()
-
-const multisendInterface = Multi_send__factory.createInterface()
-
describe('transactions', () => {
- const mockGetAndValidateSdk = getAndValidateSafeSDK as jest.MockedFunction
-
describe('getQueuedTransactionCount', () => {
it('should return 0 if no txPage is provided', () => {
expect(getQueuedTransactionCount()).toBe('0')
@@ -226,297 +196,4 @@ describe('transactions', () => {
).toBe(false)
})
})
-
- describe('prependSafeToL2Migration', () => {
- beforeEach(() => {
- // Mock create Tx
- mockGetAndValidateSdk.mockReturnValue({
- createTransaction: ({ transactions, onlyCalls }) => {
- return Promise.resolve(
- safeTxBuilder()
- .with({
- data: safeTxDataBuilder()
- .with({
- to: onlyCalls
- ? (getMultiSendCallOnlyDeployment()?.defaultAddress ?? faker.finance.ethereumAddress())
- : (getMultiSendDeployment()?.defaultAddress ?? faker.finance.ethereumAddress()),
- value: '0',
- data: Multi_send__factory.createInterface().encodeFunctionData('multiSend', [
- encodeMultiSendData(transactions),
- ]),
- nonce: 0,
- operation: 1,
- })
- .build(),
- })
- .build(),
- )
- },
- } as Safe)
- })
-
- it('should return undefined for undefined safeTx', () => {
- expect(
- prependSafeToL2Migration(undefined, extendedSafeInfoBuilder().build(), chainBuilder().build()),
- ).resolves.toBeUndefined()
- })
-
- it('should throw if chain is undefined', () => {
- expect(() => prependSafeToL2Migration(undefined, extendedSafeInfoBuilder().build(), undefined)).toThrowError()
- })
-
- it('should not modify tx if the chain is L1', () => {
- const safeTx = safeTxBuilder()
- .with({ data: safeTxDataBuilder().with({ nonce: 0 }).build() })
- .build()
-
- const safeInfo = extendedSafeInfoBuilder()
- .with({ implementationVersionState: ImplementationVersionState.UNKNOWN })
- .build()
-
- expect(prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: false }).build())).resolves.toEqual(
- safeTx,
- )
- })
-
- it('should not modify tx if the nonce is > 0', () => {
- const safeTx = safeTxBuilder()
- .with({ data: safeTxDataBuilder().with({ nonce: 1 }).build() })
- .build()
-
- const safeInfo = extendedSafeInfoBuilder()
- .with({ implementationVersionState: ImplementationVersionState.UNKNOWN })
- .build()
-
- expect(prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true }).build())).resolves.toEqual(
- safeTx,
- )
- })
-
- it('should not modify tx if implementationState is correct', () => {
- const safeTx = safeTxBuilder()
- .with({ data: safeTxDataBuilder().with({ nonce: 0 }).build() })
- .build()
-
- const safeInfo = extendedSafeInfoBuilder()
- .with({ implementationVersionState: ImplementationVersionState.UP_TO_DATE })
- .build()
- expect(prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true }).build())).resolves.toEqual(
- safeTx,
- )
- })
-
- it('should not modify tx if the tx is already signed', () => {
- const safeTx = safeTxBuilder()
- .with({ data: safeTxDataBuilder().with({ nonce: 0 }).build() })
- .build()
-
- safeTx.addSignature(safeSignatureBuilder().build())
-
- const safeInfo = extendedSafeInfoBuilder()
- .with({ implementationVersionState: ImplementationVersionState.UNKNOWN })
- .build()
-
- expect(prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true }).build())).resolves.toEqual(
- safeTx,
- )
- })
-
- it('should not modify tx if the chain has no migration lib deployed', () => {
- const safeTx = safeTxBuilder()
- .with({ data: safeTxDataBuilder().with({ nonce: 0 }).build() })
- .build()
-
- const safeInfo = extendedSafeInfoBuilder()
- .with({ implementationVersionState: ImplementationVersionState.UNKNOWN })
- .build()
-
- expect(
- prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true, chainId: '69420' }).build()),
- ).resolves.toEqual(safeTx)
- })
-
- it('should not modify tx if the tx already migrates', () => {
- const safeL2SingletonDeployment = getSafeL2SingletonDeployment()?.defaultAddress
-
- const safeTx = safeTxBuilder()
- .with({
- data: safeTxDataBuilder()
- .with({
- nonce: 0,
- to: safeToL2MigrationAddress,
- data:
- safeL2SingletonDeployment &&
- safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [safeL2SingletonDeployment]),
- })
- .build(),
- })
- .build()
- const safeInfo = extendedSafeInfoBuilder()
- .with({
- implementationVersionState: ImplementationVersionState.UNKNOWN,
- implementation: {
- name: '1.3.0',
- value: getSafeSingletonDeployment()?.defaultAddress ?? faker.finance.ethereumAddress(),
- },
- })
- .build()
- expect(
- prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true, chainId: '10' }).build()),
- ).resolves.toEqual(safeTx)
- const multiSendSafeTx = safeTxBuilder()
- .with({
- data: safeTxDataBuilder()
- .with({
- nonce: 0,
- to: getMultiSendDeployment()?.defaultAddress,
- data:
- safeToL2MigrationAddress &&
- safeL2SingletonDeployment &&
- Multi_send__factory.createInterface().encodeFunctionData('multiSend', [
- encodeMultiSendData([
- {
- value: '0',
- operation: 1,
- to: safeToL2MigrationAddress,
- data: safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [safeL2SingletonDeployment]),
- },
- ]),
- ]),
- })
- .build(),
- })
- .build()
- expect(
- prependSafeToL2Migration(multiSendSafeTx, safeInfo, chainBuilder().with({ l2: true, chainId: '10' }).build()),
- ).resolves.toEqual(multiSendSafeTx)
- })
-
- it('should modify single txs if applicable', async () => {
- const safeTx = safeTxBuilder()
- .with({
- data: safeTxDataBuilder()
- .with({
- nonce: 0,
- to: faker.finance.ethereumAddress(),
- data: faker.string.hexadecimal({ length: 10 }),
- value: '0',
- })
- .build(),
- })
- .build()
-
- const safeInfo = extendedSafeInfoBuilder()
- .with({
- implementationVersionState: ImplementationVersionState.UNKNOWN,
- implementation: {
- name: '1.3.0',
- value: getSafeSingletonDeployment()?.defaultAddress ?? faker.finance.ethereumAddress(),
- },
- })
- .build()
-
- const modifiedTx = await prependSafeToL2Migration(
- safeTx,
- safeInfo,
- chainBuilder().with({ l2: true, chainId: '10' }).build(),
- )
-
- expect(modifiedTx).not.toEqual(safeTx)
- expect(modifiedTx?.data.to).toEqual(getMultiSendDeployment()?.defaultAddress)
- const decodedMultiSend = decodeMultiSendData(modifiedTx!.data.data)
- expect(decodedMultiSend).toHaveLength(2)
- const safeL2SingletonDeployment = getSafeL2SingletonDeployment()?.defaultAddress
-
- expect(decodedMultiSend).toEqual([
- {
- to: safeToL2MigrationAddress,
- value: '0',
- operation: 1,
- data:
- safeL2SingletonDeployment &&
- safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [safeL2SingletonDeployment]),
- },
- {
- to: checksumAddress(safeTx.data.to),
- value: safeTx.data.value,
- operation: safeTx.data.operation,
- data: safeTx.data.data.toLowerCase(),
- },
- ])
- })
- })
-
- describe('extractMigrationL2MasterCopyAddress', () => {
- it('should return undefined for undefined safeTx', () => {
- expect(extractMigrationL2MasterCopyAddress(undefined)).toBeUndefined()
- })
-
- it('should return undefined for non multisend safeTx', () => {
- expect(extractMigrationL2MasterCopyAddress(safeTxBuilder().build())).toBeUndefined()
- })
-
- it('should return undefined for multisend without migration', () => {
- expect(
- extractMigrationL2MasterCopyAddress(
- safeTxBuilder()
- .with({
- data: safeTxDataBuilder()
- .with({
- data: multisendInterface.encodeFunctionData('multiSend', [
- encodeMultiSendData([
- {
- to: faker.finance.ethereumAddress(),
- data: faker.string.hexadecimal({ length: 64 }),
- value: '0',
- operation: 0,
- },
- {
- to: faker.finance.ethereumAddress(),
- data: faker.string.hexadecimal({ length: 64 }),
- value: '0',
- operation: 0,
- },
- ]),
- ]),
- })
- .build(),
- })
- .build(),
- ),
- ).toBeUndefined()
- })
-
- it('should return migration address for multisend with migration as first tx', () => {
- const l2SingletonAddress = getSafeL2SingletonDeployment()?.defaultAddress!
- expect(
- extractMigrationL2MasterCopyAddress(
- safeTxBuilder()
- .with({
- data: safeTxDataBuilder()
- .with({
- data: multisendInterface.encodeFunctionData('multiSend', [
- encodeMultiSendData([
- {
- to: safeToL2MigrationAddress!,
- data: safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [l2SingletonAddress]),
- value: '0',
- operation: 1,
- },
- {
- to: faker.finance.ethereumAddress(),
- data: faker.string.hexadecimal({ length: 64 }),
- value: '0',
- operation: 0,
- },
- ]),
- ]),
- })
- .build(),
- })
- .build(),
- ),
- ).toEqual(l2SingletonAddress)
- })
- })
})
diff --git a/src/utils/chains.ts b/src/utils/chains.ts
index d1aa5bcf27..971212b90d 100644
--- a/src/utils/chains.ts
+++ b/src/utils/chains.ts
@@ -32,7 +32,6 @@ export enum FEATURES {
NATIVE_SWAPS_USE_COW_STAGING_SERVER = 'NATIVE_SWAPS_USE_COW_STAGING_SERVER',
NATIVE_SWAPS_FEE_ENABLED = 'NATIVE_SWAPS_FEE_ENABLED',
ZODIAC_ROLES = 'ZODIAC_ROLES',
- SAFE_141 = 'SAFE_141',
STAKING = 'STAKING',
STAKING_BANNER = 'STAKING_BANNER',
MULTI_CHAIN_SAFE_CREATION = 'MULTI_CHAIN_SAFE_CREATION',
@@ -71,7 +70,7 @@ export const isRouteEnabled = (route: string, chain?: ChainInfo) => {
}
export const getLatestSafeVersion = (chain: ChainInfo | undefined): SafeVersion => {
- const latestSafeVersion = chain && hasFeature(chain, FEATURES.SAFE_141) ? LATEST_SAFE_VERSION : FALLBACK_SAFE_VERSION
+ const latestSafeVersion = chain?.recommendedMasterCopyVersion || LATEST_SAFE_VERSION
// Without version filter it will always return the LATEST_SAFE_VERSION constant to avoid automatically updating to the newest version if the deployments change
const latestDeploymentVersion = (getSafeSingletonDeployment({ network: chain?.chainId, released: true })?.version ??
FALLBACK_SAFE_VERSION) as SafeVersion
diff --git a/src/utils/safe-migrations.ts b/src/utils/safe-migrations.ts
new file mode 100644
index 0000000000..10fd10df5f
--- /dev/null
+++ b/src/utils/safe-migrations.ts
@@ -0,0 +1,147 @@
+import { Safe_to_l2_migration__factory, Safe_migration__factory } from '@/types/contracts'
+import { type ExtendedSafeInfo } from '@/store/safeInfoSlice'
+import { getSafeContractDeployment } from '@/services/contracts/deployments'
+import { sameAddress } from './addresses'
+import { getSafeToL2MigrationDeployment, getSafeMigrationDeployment } from '@safe-global/safe-deployments'
+import { type MetaTransactionData, OperationType, type SafeTransaction } from '@safe-global/safe-core-sdk-types'
+import type { ChainInfo, TransactionData } from '@safe-global/safe-gateway-typescript-sdk'
+import { isValidMasterCopy } from '@/services/contracts/safeContracts'
+import { isMultiSendCalldata } from './transaction-calldata'
+import { decodeMultiSendData } from '@safe-global/protocol-kit/dist/src/utils'
+import { __unsafe_createMultiSendTx } from '@/services/tx/tx-sender'
+import { LATEST_SAFE_VERSION } from '@/config/constants'
+
+/**
+ *
+ * If the Safe is using a invalid masterCopy this function will modify the passed in `safeTx` by making it a MultiSend that migrates the Safe to L2 as the first action.
+ *
+ * This only happens under the conditions that
+ * - The Safe's nonce is 0
+ * - The SafeTx's nonce is 0
+ * - The Safe is using an invalid masterCopy
+ * - The SafeTx is not already including a Migration
+ *
+ * @param safeTx original SafeTx
+ * @param safe
+ * @param chain
+ * @returns
+ */
+export const prependSafeToL2Migration = (
+ safeTx: SafeTransaction | undefined,
+ safe: ExtendedSafeInfo,
+ chain: ChainInfo | undefined,
+): Promise => {
+ if (!chain) {
+ throw new Error('No Network information available')
+ }
+
+ const safeL2Deployment = getSafeContractDeployment(chain, safe.version)
+ const safeL2DeploymentAddress = safeL2Deployment?.networkAddresses[chain.chainId]
+ const safeToL2MigrationDeployment = getSafeToL2MigrationDeployment({ network: chain.chainId })
+ const safeToL2MigrationAddress = safeToL2MigrationDeployment?.networkAddresses[chain.chainId]
+
+ if (
+ !safeTx ||
+ safeTx.signatures.size > 0 ||
+ !chain.l2 ||
+ safeTx.data.nonce > 0 ||
+ isValidMasterCopy(safe.implementationVersionState) ||
+ !safeToL2MigrationAddress ||
+ !safeL2DeploymentAddress
+ ) {
+ // We do not migrate on L1s
+ // We cannot migrate if the nonce is > 0
+ // We do not modify already signed txs
+ // We do not modify supported masterCopies
+ // We cannot migrate if no migration contract or L2 contract exists
+ return Promise.resolve(safeTx)
+ }
+
+ const safeToL2MigrationInterface = Safe_to_l2_migration__factory.createInterface()
+
+ if (sameAddress(safe.implementation.value, safeL2DeploymentAddress)) {
+ // Safe already has the correct L2 masterCopy
+ // This should in theory never happen if the implementationState is valid
+ return Promise.resolve(safeTx)
+ }
+
+ // If the Safe is a L1 masterCopy on a L2 network and still has nonce 0, we prepend a call to the migration contract to the safeTx.
+ const txData = safeTx.data.data
+
+ let internalTxs: MetaTransactionData[]
+ if (isMultiSendCalldata(txData)) {
+ // Check if the first tx is already a call to the migration contract
+ internalTxs = decodeMultiSendData(txData)
+ } else {
+ internalTxs = [{ to: safeTx.data.to, operation: safeTx.data.operation, value: safeTx.data.value, data: txData }]
+ }
+
+ if (sameAddress(internalTxs[0]?.to, safeToL2MigrationAddress)) {
+ // We already migrate. Nothing to do.
+ return Promise.resolve(safeTx)
+ }
+
+ // Prepend the migration tx
+ const newTxs: MetaTransactionData[] = [
+ {
+ operation: OperationType.DelegateCall, // DELEGATE CALL REQUIRED
+ data: safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [safeL2DeploymentAddress]),
+ to: safeToL2MigrationAddress,
+ value: '0',
+ },
+ ...internalTxs,
+ ]
+
+ return __unsafe_createMultiSendTx(newTxs)
+}
+
+export const extractMigrationL2MasterCopyAddress = (txData: TransactionData): string | undefined => {
+ if (!isMultiSendCalldata(txData.hexData || '')) {
+ return undefined
+ }
+
+ const innerTxs = decodeMultiSendData(txData.hexData || '')
+ const firstInnerTx = innerTxs[0]
+ if (!firstInnerTx) {
+ return undefined
+ }
+
+ const safeToL2MigrationDeployment = getSafeToL2MigrationDeployment()
+ const safeToL2MigrationAddress = safeToL2MigrationDeployment?.defaultAddress
+ const safeToL2MigrationInterface = Safe_to_l2_migration__factory.createInterface()
+
+ if (
+ firstInnerTx.data.startsWith(safeToL2MigrationInterface.getFunction('migrateToL2').selector) &&
+ sameAddress(firstInnerTx.to, safeToL2MigrationAddress)
+ ) {
+ const callParams = safeToL2MigrationInterface.decodeFunctionData('migrateToL2', firstInnerTx.data)
+ return callParams[0]
+ }
+
+ return undefined
+}
+
+export const createUpdateMigration = (chain: ChainInfo): MetaTransactionData => {
+ const interfce = Safe_migration__factory.createInterface()
+
+ const deployment = getSafeMigrationDeployment({
+ version: chain.recommendedMasterCopyVersion || LATEST_SAFE_VERSION,
+ released: true,
+ network: chain.chainId,
+ })
+
+ if (!deployment) {
+ throw new Error('Migration deployment not found')
+ }
+
+ const tx: MetaTransactionData = {
+ operation: OperationType.DelegateCall, // DELEGATE CALL REQUIRED
+ data: chain.l2
+ ? interfce.encodeFunctionData('migrateL2WithFallbackHandler')
+ : interfce.encodeFunctionData('migrateWithFallbackHandler'),
+ to: deployment.defaultAddress,
+ value: '0',
+ }
+
+ return tx
+}
diff --git a/src/utils/transaction-guards.ts b/src/utils/transaction-guards.ts
index f126ebf5b0..7e63765c4e 100644
--- a/src/utils/transaction-guards.ts
+++ b/src/utils/transaction-guards.ts
@@ -46,6 +46,7 @@ import {
ConfirmationViewTypes,
ConflictType,
DetailedExecutionInfoType,
+ Operation,
TransactionInfoType,
TransactionListItemType,
TransactionStatus,
@@ -57,12 +58,18 @@ import { sameAddress } from '@/utils/addresses'
import type { NamedAddress } from '@/components/new-safe/create/types'
import type { RecoveryQueueItem } from '@/features/recovery/services/recovery-state'
import { ethers } from 'ethers'
-import { getSafeToL2MigrationDeployment, getMultiSendDeployments } from '@safe-global/safe-deployments'
+import {
+ getSafeToL2MigrationDeployment,
+ getSafeMigrationDeployment,
+ getMultiSendDeployments,
+} from '@safe-global/safe-deployments'
import { Safe__factory, Safe_to_l2_migration__factory } from '@/types/contracts'
import { hasMatchingDeployment } from '@/services/contracts/deployments'
import { isMultiSendCalldata } from './transaction-calldata'
import { decodeMultiSendData } from '@safe-global/protocol-kit/dist/src/utils'
import { OperationType } from '@safe-global/safe-core-sdk-types'
+import { LATEST_SAFE_VERSION } from '@/config/constants'
+import { extractMigrationL2MasterCopyAddress } from './safe-migrations'
export const isTxQueued = (value: TransactionStatus): boolean => {
return [TransactionStatus.AWAITING_CONFIRMATIONS, TransactionStatus.AWAITING_EXECUTION].includes(value)
@@ -447,3 +454,31 @@ export const isExecTxInfo = (info: TransactionInfo): info is Custom => {
export const isNestedConfirmationTxInfo = (info: TransactionInfo): boolean => {
return isCustomTxInfo(info) && (isOnChainConfirmationTxInfo(info) || isExecTxInfo(info))
}
+
+export const isSafeUpdateTxData = (data?: TransactionData): boolean => {
+ if (!data) return false
+
+ // Must be a trusted delegate call
+ if (!(data.trustedDelegateCallTarget && data.operation === Operation.DELEGATE)) {
+ return false
+ }
+
+ // For 1.3.0+ Safes
+ const migrationContract = getSafeMigrationDeployment({ version: LATEST_SAFE_VERSION })
+ if (migrationContract && sameAddress(data.to.value, migrationContract.defaultAddress)) {
+ return true
+ }
+
+ // For older Safes
+ return (
+ isMultiSendCalldata(data.hexData || '') &&
+ Boolean(
+ data.dataDecoded?.parameters?.[0]?.valueDecoded?.some((tx) => tx.dataDecoded?.method === 'changeMasterCopy'),
+ )
+ )
+}
+
+export const isSafeToL2MigrationTxData = (data?: TransactionData): boolean => {
+ if (!data) return false
+ return !!extractMigrationL2MasterCopyAddress(data)
+}
diff --git a/src/utils/transactions.ts b/src/utils/transactions.ts
index 4348270044..fda843b7d3 100644
--- a/src/utils/transactions.ts
+++ b/src/utils/transactions.ts
@@ -21,24 +21,18 @@ import {
} from './transaction-guards'
import type { MetaTransactionData } from '@safe-global/safe-core-sdk-types/dist/src/types'
import { OperationType } from '@safe-global/safe-core-sdk-types/dist/src/types'
-import { getReadOnlyGnosisSafeContract, isValidMasterCopy } from '@/services/contracts/safeContracts'
+import { getReadOnlyGnosisSafeContract } from '@/services/contracts/safeContracts'
import extractTxInfo from '@/services/tx/extractTxInfo'
import type { AdvancedParameters } from '@/components/tx/AdvancedParams'
import type { SafeTransaction, TransactionOptions } from '@safe-global/safe-core-sdk-types'
import { FEATURES, hasFeature } from '@/utils/chains'
import uniqBy from 'lodash/uniqBy'
import { Errors, logError } from '@/services/exceptions'
-import { Safe_to_l2_migration__factory } from '@/types/contracts'
import { type BaseTransaction } from '@safe-global/safe-apps-sdk'
import { isEmptyHexData } from '@/utils/hex'
-import { type ExtendedSafeInfo } from '@/store/safeInfoSlice'
-import { getSafeContractDeployment } from '@/services/contracts/deployments'
-import { sameAddress } from './addresses'
import { isMultiSendCalldata } from './transaction-calldata'
import { decodeMultiSendData } from '@safe-global/protocol-kit/dist/src/utils'
-import { __unsafe_createMultiSendTx } from '@/services/tx/tx-sender'
import { getOriginPath } from './url'
-import { getSafeToL2MigrationDeployment } from '@safe-global/safe-deployments'
export const makeTxFromDetails = (txDetails: TransactionDetails): Transaction => {
const getMissingSigners = ({
@@ -237,120 +231,6 @@ export const isImitation = ({ txInfo }: TransactionSummary): boolean => {
return isTransferTxInfo(txInfo) && isERC20Transfer(txInfo.transferInfo) && Boolean(txInfo.transferInfo.imitation)
}
-/**
- *
- * If the Safe is using a invalid masterCopy this function will modify the passed in `safeTx` by making it a MultiSend that migrates the Safe to L2 as the first action.
- *
- * This only happens under the conditions that
- * - The Safe's nonce is 0
- * - The SafeTx's nonce is 0
- * - The Safe is using an invalid masterCopy
- * - The SafeTx is not already including a Migration
- *
- * @param safeTx original SafeTx
- * @param safe
- * @param chain
- * @returns
- */
-export const prependSafeToL2Migration = (
- safeTx: SafeTransaction | undefined,
- safe: ExtendedSafeInfo,
- chain: ChainInfo | undefined,
-): Promise => {
- if (!chain) {
- throw new Error('No Network information available')
- }
-
- const safeL2Deployment = getSafeContractDeployment(chain, safe.version)
- const safeL2DeploymentAddress = safeL2Deployment?.networkAddresses[chain.chainId]
- const safeToL2MigrationDeployment = getSafeToL2MigrationDeployment({ network: chain.chainId })
- const safeToL2MigrationAddress = safeToL2MigrationDeployment?.networkAddresses[chain.chainId]
-
- if (
- !safeTx ||
- safeTx.signatures.size > 0 ||
- !chain.l2 ||
- safeTx.data.nonce > 0 ||
- isValidMasterCopy(safe.implementationVersionState) ||
- !safeToL2MigrationAddress ||
- !safeL2DeploymentAddress
- ) {
- // We do not migrate on L1s
- // We cannot migrate if the nonce is > 0
- // We do not modify already signed txs
- // We do not modify supported masterCopies
- // We cannot migrate if no migration contract or L2 contract exists
- return Promise.resolve(safeTx)
- }
-
- const safeToL2MigrationInterface = Safe_to_l2_migration__factory.createInterface()
-
- if (sameAddress(safe.implementation.value, safeL2DeploymentAddress)) {
- // Safe already has the correct L2 masterCopy
- // This should in theory never happen if the implementationState is valid
- return Promise.resolve(safeTx)
- }
-
- // If the Safe is a L1 masterCopy on a L2 network and still has nonce 0, we prepend a call to the migration contract to the safeTx.
- const txData = safeTx.data.data
-
- let internalTxs: MetaTransactionData[]
- if (isMultiSendCalldata(txData)) {
- // Check if the first tx is already a call to the migration contract
- internalTxs = decodeMultiSendData(txData)
- } else {
- internalTxs = [{ to: safeTx.data.to, operation: safeTx.data.operation, value: safeTx.data.value, data: txData }]
- }
-
- if (sameAddress(internalTxs[0]?.to, safeToL2MigrationAddress)) {
- // We already migrate. Nothing to do.
- return Promise.resolve(safeTx)
- }
-
- // Prepend the migration tx
- const newTxs: MetaTransactionData[] = [
- {
- operation: 1, // DELEGATE CALL REQUIRED
- data: safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [safeL2DeploymentAddress]),
- to: safeToL2MigrationAddress,
- value: '0',
- },
- ...internalTxs,
- ]
-
- return __unsafe_createMultiSendTx(newTxs)
-}
-
-export const extractMigrationL2MasterCopyAddress = (safeTx: SafeTransaction | undefined): string | undefined => {
- if (!safeTx) {
- return undefined
- }
-
- if (!isMultiSendCalldata(safeTx.data.data)) {
- return undefined
- }
-
- const innerTxs = decodeMultiSendData(safeTx.data.data)
- const firstInnerTx = innerTxs[0]
- if (!firstInnerTx) {
- return undefined
- }
-
- const safeToL2MigrationDeployment = getSafeToL2MigrationDeployment()
- const safeToL2MigrationAddress = safeToL2MigrationDeployment?.defaultAddress
- const safeToL2MigrationInterface = Safe_to_l2_migration__factory.createInterface()
-
- if (
- firstInnerTx.data.startsWith(safeToL2MigrationInterface.getFunction('migrateToL2').selector) &&
- sameAddress(firstInnerTx.to, safeToL2MigrationAddress)
- ) {
- const callParams = safeToL2MigrationInterface.decodeFunctionData('migrateToL2', firstInnerTx.data)
- return callParams[0]
- }
-
- return undefined
-}
-
export const getSafeTransaction = async (safeTxHash: string, chainId: string, safeAddress: string) => {
const txId = `multisig_${safeAddress}_${safeTxHash}`
diff --git a/yarn.lock b/yarn.lock
index 12931d965b..a9abaeb7a1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5820,7 +5820,14 @@ __metadata:
languageName: node
linkType: hard
-"@safe-global/safe-gateway-typescript-sdk@npm:3.22.4, @safe-global/safe-gateway-typescript-sdk@npm:^3.5.3":
+"@safe-global/safe-gateway-typescript-sdk@npm:3.22.5-beta.0":
+ version: 3.22.5-beta.0
+ resolution: "@safe-global/safe-gateway-typescript-sdk@npm:3.22.5-beta.0"
+ checksum: 10/503bfeb717a23bcd6fffd087c3365b24497f8712b772a006134dd032bc6f36e9681be3cfb8e52ee915fe1d5c1945e746bbcdd8f456a8ad25194695d4bdfc13d4
+ languageName: node
+ linkType: hard
+
+"@safe-global/safe-gateway-typescript-sdk@npm:^3.5.3":
version: 3.22.4
resolution: "@safe-global/safe-gateway-typescript-sdk@npm:3.22.4"
checksum: 10/5b088499a01a0d0190b4ab6828bfb2df779b603bbcee7645c23ad8e420670aab4ce7ca39b858fc62ee03fded77b322c3f8a9b0203f41ecb779d08f47bd4bfe0c
@@ -21976,7 +21983,7 @@ __metadata:
"@safe-global/safe-apps-sdk": "npm:^9.1.0"
"@safe-global/safe-client-gateway-sdk": "npm:v1.60.1"
"@safe-global/safe-core-sdk-types": "npm:^5.0.1"
- "@safe-global/safe-gateway-typescript-sdk": "npm:3.22.4"
+ "@safe-global/safe-gateway-typescript-sdk": "npm:3.22.5-beta.0"
"@safe-global/safe-modules-deployments": "npm:^2.2.1"
"@sentry/react": "npm:^7.91.0"
"@sentry/types": "npm:^7.74.0"