diff --git a/public/images/sidebar/subaccounts-icon.svg b/public/images/sidebar/subaccounts-icon.svg index b2758c6f24..ee7ca94938 100644 --- a/public/images/sidebar/subaccounts-icon.svg +++ b/public/images/sidebar/subaccounts-icon.svg @@ -1,8 +1,3 @@ - - - - - - + diff --git a/src/components/sidebar/SafeListContextMenu/index.tsx b/src/components/sidebar/SafeListContextMenu/index.tsx index daffce9e53..932bae11b6 100644 --- a/src/components/sidebar/SafeListContextMenu/index.tsx +++ b/src/components/sidebar/SafeListContextMenu/index.tsx @@ -92,7 +92,7 @@ const SafeListContextMenu = ({ {!undeployedSafe && subaccounts?.safes && subaccounts.safes.length > 0 && ( diff --git a/src/components/sidebar/SidebarHeader/index.tsx b/src/components/sidebar/SidebarHeader/index.tsx index 95f69c9226..67fc56e1f0 100644 --- a/src/components/sidebar/SidebarHeader/index.tsx +++ b/src/components/sidebar/SidebarHeader/index.tsx @@ -116,7 +116,7 @@ const SafeHeader = (): ReactElement => { - + diff --git a/src/components/sidebar/SubaccountsList/index.tsx b/src/components/sidebar/SubaccountsList/index.tsx index c796fd08f9..abc8b939ea 100644 --- a/src/components/sidebar/SubaccountsList/index.tsx +++ b/src/components/sidebar/SubaccountsList/index.tsx @@ -18,6 +18,7 @@ export function SubaccountsList({ subaccounts }: { subaccounts: Array }) return ( {subaccountsToShow.map((subaccount) => { + // TODO: Turn into link to Subaccount return ( }) }} key={subaccount} > - + ) @@ -49,6 +50,7 @@ export function SubaccountsList({ subaccounts }: { subaccounts: Array }) color="text.secondary" sx={{ textTransform: 'uppercase', + fontWeight: 700, }} onClick={onShowAll} > diff --git a/src/components/tx-flow/flows/CreateSubaccount/ReviewSubaccount.tsx b/src/components/tx-flow/flows/CreateSubaccount/ReviewSubaccount.tsx index 7fce624f9e..5ced62f773 100644 --- a/src/components/tx-flow/flows/CreateSubaccount/ReviewSubaccount.tsx +++ b/src/components/tx-flow/flows/CreateSubaccount/ReviewSubaccount.tsx @@ -1,83 +1,101 @@ -import { useContext, useEffect } from 'react' +import { useContext, useEffect, useMemo } from 'react' import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm' import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider' -import { createSubaccount } from '@/components/tx-flow/flows/CreateSubaccount/create-subaccount-tx' import useSafeInfo from '@/hooks/useSafeInfo' import useBalances from '@/hooks/useBalances' import { useCurrentChain } from '@/hooks/useChains' -import useWallet from '@/hooks/wallets/useWallet' import { useAppDispatch } from '@/store' import { upsertAddressBookEntries } from '@/store/addressBookSlice' import { getLatestSafeVersion } from '@/utils/chains' -import { getReadOnlyFallbackHandlerContract } from '@/services/contracts/safeContracts' import useAsync from '@/hooks/useAsync' -import { computeNewSafeAddress } from '@/components/new-safe/create/logic' -import type { SetupSubaccountForm } from '@/components/tx-flow/flows/CreateSubaccount/SetupSubaccount' +import { createNewUndeployedSafeWithoutSalt, encodeSafeCreationTx } from '@/components/new-safe/create/logic' +import { + SetupSubaccountFormAssetFields, + type SetupSubaccountForm, +} from '@/components/tx-flow/flows/CreateSubaccount/SetupSubaccount' import { useGetSafesByOwnerQuery } from '@/store/slices' +import { predictAddressBasedOnReplayData } from '@/features/multichain/utils/utils' +import { useWeb3ReadOnly } from '@/hooks/wallets/web3' +import { createTokenTransferParams } from '@/services/tx/tokenTransferParams' +import { createMultiSendCallOnlyTx, createTx } from '@/services/tx/tx-sender' +import type { SafeTransaction } from '@safe-global/safe-core-sdk-types' +import EthHashInfo from '@/components/common/EthHashInfo' +import { Grid, Typography } from '@mui/material' export function ReviewSubaccount({ params }: { params: SetupSubaccountForm }) { const dispatch = useAppDispatch() - const wallet = useWallet() const { safeAddress, safe } = useSafeInfo() const chain = useCurrentChain() const { setSafeTx, setSafeTxError } = useContext(SafeTxContext) const { balances } = useBalances() - const safeVersion = getLatestSafeVersion(chain) + const provider = useWeb3ReadOnly() const { data: subaccounts } = useGetSafesByOwnerQuery({ chainId: safe.chainId, ownerAddress: safe.address.value }) - const saltNonce = (subaccounts?.safes.length ?? 0).toString() + const version = getLatestSafeVersion(chain) - const [safeAccountConfig] = useAsync(async () => { - const fallbackHandler = await getReadOnlyFallbackHandlerContract(safeVersion) - const owners = [safeAddress] - return { - owners, - threshold: owners.length, - fallbackHandler: fallbackHandler?.contractAddress, - } - }, [safeVersion, safeAddress]) - - const [predictedSafeAddress] = useAsync(async () => { - if (!wallet?.provider || !safeAccountConfig || !chain || !safeVersion) { + const safeAccountConfig = useMemo(() => { + if (!chain || !subaccounts) { return } - return computeNewSafeAddress( - wallet.provider, + + const undeployedSafe = createNewUndeployedSafeWithoutSalt( + version, { - safeAccountConfig, - saltNonce, + owners: [safeAddress], + threshold: 1, }, chain, - safeVersion, ) - }, [wallet?.provider, safeAccountConfig, chain, safeVersion, saltNonce]) + const saltNonce = subaccounts.safes.length.toString() + + return { + ...undeployedSafe, + saltNonce, + } + }, [chain, safeAddress, subaccounts, version]) + + const [predictedSafeAddress] = useAsync(async () => { + if (provider && safeAccountConfig) { + return predictAddressBasedOnReplayData(safeAccountConfig, provider) + } + }, [provider, safeAccountConfig]) useEffect(() => { - if (!wallet?.provider || !safeAccountConfig || !predictedSafeAddress) { + if (!chain || !safeAccountConfig || !predictedSafeAddress) { return } - createSubaccount({ - provider: wallet.provider, - assets: params.assets, - safeAccountConfig, - safeDeploymentConfig: { - saltNonce, - }, - predictedSafeAddress, - balances, - }) - .then(setSafeTx) - .catch(setSafeTxError) - }, [ - wallet?.provider, - params.assets, - safeAccountConfig, - predictedSafeAddress, - balances, - setSafeTx, - setSafeTxError, - saltNonce, - ]) + + const deploymentTx = { + to: safeAccountConfig.factoryAddress, + data: encodeSafeCreationTx(safeAccountConfig, chain), + value: '0', + } + + const fundingTxs = params.assets + .map((asset) => { + const token = balances.items.find((item) => { + return item.tokenInfo.address === asset[SetupSubaccountFormAssetFields.tokenAddress] + }) + if (token) { + return createTokenTransferParams( + predictedSafeAddress, + asset[SetupSubaccountFormAssetFields.amount], + token.tokenInfo.decimals, + token.tokenInfo.address, + ) + } + }) + .filter((tx) => { + return tx != null + }) + + const createSafeTx = async (): Promise => { + const isMultiSend = fundingTxs.length > 0 + return isMultiSend ? createMultiSendCallOnlyTx([deploymentTx, ...fundingTxs]) : createTx(deploymentTx) + } + + createSafeTx().then(setSafeTx).catch(setSafeTxError) + }, [chain, params.assets, safeAccountConfig, predictedSafeAddress, balances.items, setSafeTx, setSafeTxError]) const onSubmit = () => { if (!predictedSafeAddress) { @@ -92,5 +110,38 @@ export function ReviewSubaccount({ params }: { params: SetupSubaccountForm }) { ) } - return + return ( + + {predictedSafeAddress && ( + + + + Subaccount + + + + + + + + )} + + ) } diff --git a/src/components/tx-flow/flows/CreateSubaccount/SetupSubaccount.tsx b/src/components/tx-flow/flows/CreateSubaccount/SetupSubaccount.tsx index ada305d557..0b52d39a2a 100644 --- a/src/components/tx-flow/flows/CreateSubaccount/SetupSubaccount.tsx +++ b/src/components/tx-flow/flows/CreateSubaccount/SetupSubaccount.tsx @@ -20,7 +20,8 @@ import InfoIcon from '@/public/images/notifications/info.svg' import AddIcon from '@/public/images/common/add.svg' import DeleteIcon from '@/public/images/common/delete.svg' import TxCard from '@/components/tx-flow/common/TxCard' -import { useMnemonicSafeName } from '@/hooks/useMnemonicName' +import useSafeAddress from '@/hooks/useSafeAddress' +import useAddressBook from '@/hooks/useAddressBook' import NameInput from '@/components/common/NameInput' import tokenInputCss from '@/components/common/TokenAmountInput/styles.module.css' import NumberField from '@/components/common/NumberField' @@ -54,7 +55,10 @@ export function SetUpSubaccount({ params: SetupSubaccountForm onSubmit: (params: SetupSubaccountForm) => void }) { - const fallbackName = useMnemonicSafeName() + const addressBook = useAddressBook() + const safeAddress = useSafeAddress() + const fallbackName = `${addressBook[safeAddress]} Subaccount` + const formMethods = useForm({ defaultValues: params, mode: 'onChange', diff --git a/src/components/tx-flow/flows/CreateSubaccount/create-subaccount-tx.ts b/src/components/tx-flow/flows/CreateSubaccount/create-subaccount-tx.ts deleted file mode 100644 index 4138de460c..0000000000 --- a/src/components/tx-flow/flows/CreateSubaccount/create-subaccount-tx.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { OperationType } from '@safe-global/safe-core-sdk-types' -import Safe from '@safe-global/protocol-kit' -import type { Eip1193Provider } from 'ethers' -import type { MetaTransactionData, SafeTransaction } from '@safe-global/safe-core-sdk-types' -import type { SafeBalanceResponse } from '@safe-global/safe-gateway-typescript-sdk' -import type { SafeAccountConfig, SafeDeploymentConfig } from '@safe-global/protocol-kit' - -import { createTokenTransferParams } from '@/services/tx/tokenTransferParams' -import { createMultiSendCallOnlyTx, createTx } from '@/services/tx/tx-sender' -import { SetupSubaccountFormAssetFields } from '@/components/tx-flow/flows/CreateSubaccount/SetupSubaccount' -import type { - SetupSubaccountForm, - SetupSubaccountFormFields, -} from '@/components/tx-flow/flows/CreateSubaccount/SetupSubaccount' - -/** - * Creates a (batch) transaction to deploy (and fund) a Subaccount. - * - * Note: Subaccounts are owned by provided {@link currentSafeAddress}, with a threshold of 1. - * - * @param {Eip1193Provider} args.provider - EIP-1193 provider - * @param {SetupSubaccountForm['assets']} args.assets - assets to fund the Subaccount with - * @param {SafeAccountConfig} args.safeAccountConfig - Subaccount configuration - * @param {SafeDeploymentConfig} args.safeDeploymentConfig - Subaccount deployment configuration - * @param {string} args.predictedSafeAddress - predicted Subaccount address - * @param {SafeBalanceResponse} args.balances - current Safe balance - * - * @returns {Promise} (batch) transaction to deploy (and fund) Subaccount - */ -export async function createSubaccount(args: { - provider: Eip1193Provider - assets: SetupSubaccountForm[SetupSubaccountFormFields.assets] - safeAccountConfig: SafeAccountConfig - safeDeploymentConfig: SafeDeploymentConfig - predictedSafeAddress: string - balances: SafeBalanceResponse -}): Promise { - const deploymentTransaction = await getDeploymentTransaction(args) - const fundingTransactions = getFundingTransactions(args) - if (fundingTransactions.length === 0) { - return createTx(deploymentTransaction) - } - return createMultiSendCallOnlyTx([deploymentTransaction, ...fundingTransactions]) -} - -/** - * Creates a transaction to deploy a Subaccount. - * - * @param {Eip1193Provider} args.provider - EIP-1193 provider - * @param {SafeAccountConfig} args.safeAccountConfig - Subaccount configuration - * @param {SafeDeploymentConfig} args.safeDeploymentConfig - Subaccount deployment configuration - * - * @returns {Promise} Safe deployment transaction - */ -async function getDeploymentTransaction(args: { - provider: Eip1193Provider - safeAccountConfig: SafeAccountConfig - safeDeploymentConfig: SafeDeploymentConfig -}): Promise { - const sdk = await Safe.init({ - provider: args.provider, - predictedSafe: { - safeAccountConfig: args.safeAccountConfig, - safeDeploymentConfig: args.safeDeploymentConfig, - }, - }) - return sdk.createSafeDeploymentTransaction().then(({ to, value, data }) => { - return { - to, - value, - data, - operation: OperationType.Call, - } - }) -} - -/** - * Creates a list of transfer transactions (to fund a Subaccount). - * - * @param {SetupSubaccountForm['assets']} args.assets - assets to fund the Subaccount - * @param {SafeBalanceResponse} args.balances - current Safe balances - * @param {string} args.predictedSafeAddress - predicted Subaccount address - * - * @returns {Array} list of transfer transactions - */ -function getFundingTransactions(args: { - assets: SetupSubaccountForm[SetupSubaccountFormFields.assets] - balances: SafeBalanceResponse - predictedSafeAddress: string -}): Array { - if (args.assets.length === 0) { - return [] - } - return args.assets - .map((asset) => { - const token = args.balances.items.find((item) => { - return item.tokenInfo.address === asset[SetupSubaccountFormAssetFields.tokenAddress] - }) - if (token) { - return createTokenTransferParams( - args.predictedSafeAddress, - asset[SetupSubaccountFormAssetFields.amount], - token.tokenInfo.decimals, - token.tokenInfo.address, - ) - } - }) - .filter((x: T): x is NonNullable => { - return x != null - }) -} diff --git a/src/components/tx-flow/flows/SuccessScreen/index.tsx b/src/components/tx-flow/flows/SuccessScreen/index.tsx index b70605ac20..a0af516d35 100644 --- a/src/components/tx-flow/flows/SuccessScreen/index.tsx +++ b/src/components/tx-flow/flows/SuccessScreen/index.tsx @@ -17,6 +17,10 @@ import { DefaultStatus } from '@/components/tx-flow/flows/SuccessScreen/statuses import { isSwapTransferOrderTxInfo } from '@/utils/transaction-guards' import { getTxLink } from '@/utils/tx-link' import useTxDetails from '@/hooks/useTxDetails' +import { usePredictSafeAddressFromTxDetails } from '@/hooks/usePredictSafeAddressFromTxDetails' +import { AppRoutes } from '@/config/routes' +import { SUBACCOUNT_EVENTS, SUBACCOUNT_LABELS } from '@/services/analytics/events/subaccounts' +import Track from '@/components/common/Track' interface Props { /** The ID assigned to the transaction in the client-gateway */ @@ -37,6 +41,7 @@ const SuccessScreen = ({ txId, txHash }: Props) => { const txLink = chain && txId && getTxLink(txId, chain, safeAddress) const [txDetails] = useTxDetails(txId) const isSwapOrder = txDetails && isSwapTransferOrderTxInfo(txDetails.txInfo) + const [predictedSafeAddress] = usePredictSafeAddressFromTxDetails(txDetails) useEffect(() => { if (!pendingTxHash) return @@ -66,13 +71,13 @@ const SuccessScreen = ({ txId, txHash }: Props) => { case PendingStatus.PROCESSING: case PendingStatus.RELAYING: // status can only have these values if txId & pendingTx are defined - StatusComponent = + StatusComponent = break case PendingStatus.INDEXING: - StatusComponent = + StatusComponent = break default: - StatusComponent = + StatusComponent = } return ( @@ -121,11 +126,30 @@ const SuccessScreen = ({ txId, txHash }: Props) => { )} - {!isSwapOrder && ( - - )} + {!isSwapOrder && + (predictedSafeAddress ? ( + + + + + + ) : ( + + ))} ) diff --git a/src/components/tx-flow/flows/SuccessScreen/statuses/DefaultStatus.tsx b/src/components/tx-flow/flows/SuccessScreen/statuses/DefaultStatus.tsx index f0c47c9fd3..71b9fc915b 100644 --- a/src/components/tx-flow/flows/SuccessScreen/statuses/DefaultStatus.tsx +++ b/src/components/tx-flow/flows/SuccessScreen/statuses/DefaultStatus.tsx @@ -4,12 +4,14 @@ import css from '@/components/tx-flow/flows/SuccessScreen/styles.module.css' import { isTimeoutError } from '@/utils/ethers-utils' const TRANSACTION_FAILED = 'Transaction failed' +const SUBACCOUNT_SUCCESSFUL = 'Subaccount was created' const TRANSACTION_SUCCESSFUL = 'Transaction was successful' type Props = { error: undefined | Error + willDeploySafe: boolean } -export const DefaultStatus = ({ error }: Props) => ( +export const DefaultStatus = ({ error, willDeploySafe: isCreatingSafe }: Props) => ( ( fontWeight: 700, }} > - {error ? TRANSACTION_FAILED : TRANSACTION_SUCCESSFUL} + {error ? TRANSACTION_FAILED : !isCreatingSafe ? TRANSACTION_SUCCESSFUL : SUBACCOUNT_SUCCESSFUL} {error && ( diff --git a/src/components/tx-flow/flows/SuccessScreen/statuses/IndexingStatus.tsx b/src/components/tx-flow/flows/SuccessScreen/statuses/IndexingStatus.tsx index 36e195a0de..5391d11e0d 100644 --- a/src/components/tx-flow/flows/SuccessScreen/statuses/IndexingStatus.tsx +++ b/src/components/tx-flow/flows/SuccessScreen/statuses/IndexingStatus.tsx @@ -2,7 +2,7 @@ import { Box, Typography } from '@mui/material' import classNames from 'classnames' import css from '@/components/tx-flow/flows/SuccessScreen/styles.module.css' -export const IndexingStatus = () => ( +export const IndexingStatus = ({ willDeploySafe: isCreatingSafe }: { willDeploySafe: boolean }) => ( ( fontWeight: 700, }} > - Transaction was processed + {!isCreatingSafe ? 'Transaction' : 'Subaccount'} was processed It is now being indexed. diff --git a/src/components/tx-flow/flows/SuccessScreen/statuses/ProcessingStatus.tsx b/src/components/tx-flow/flows/SuccessScreen/statuses/ProcessingStatus.tsx index 86c97c8918..2480ac034e 100644 --- a/src/components/tx-flow/flows/SuccessScreen/statuses/ProcessingStatus.tsx +++ b/src/components/tx-flow/flows/SuccessScreen/statuses/ProcessingStatus.tsx @@ -6,8 +6,9 @@ import { PendingStatus, type PendingTx } from '@/store/pendingTxsSlice' type Props = { txId: string pendingTx: PendingTx + willDeploySafe: boolean } -export const ProcessingStatus = ({ txId, pendingTx }: Props) => ( +export const ProcessingStatus = ({ txId, pendingTx, willDeploySafe: isCreatingSafe }: Props) => ( ( fontWeight: 700, }} > - Transaction is now processing + {!isCreatingSafe ? 'Transaction is now processing' : 'Subaccount is now being created'} ( mb: 3, }} > - The transaction was confirmed and is now being processed. + {!isCreatingSafe ? 'The transaction' : 'Your Subaccount'} was confirmed and is now being processed. {pendingTx.status === PendingStatus.PROCESSING && ( diff --git a/src/features/multichain/utils/utils.ts b/src/features/multichain/utils/utils.ts index 05384ee149..8d5da2353b 100644 --- a/src/features/multichain/utils/utils.ts +++ b/src/features/multichain/utils/utils.ts @@ -100,24 +100,34 @@ const memoizedGetProxyCreationCode = memoize( async (factoryAddress, provider) => `${factoryAddress}${(await provider.getNetwork()).chainId}`, ) -export const predictAddressBasedOnReplayData = async (safeCreationData: ReplayedSafeProps, provider: Provider) => { - const setupData = encodeSafeSetupCall(safeCreationData.safeAccountConfig) - +export const predictSafeAddress = async ( + setupData: { initializer: string; saltNonce: string; singleton: string }, + factoryAddress: string, + provider: Provider, +) => { // Step 1: Hash the initializer - const initializerHash = keccak256(setupData) + const initializerHash = keccak256(setupData.initializer) // Step 2: Encode the initializerHash and saltNonce using abi.encodePacked equivalent - const encoded = ethers.concat([initializerHash, solidityPacked(['uint256'], [safeCreationData.saltNonce])]) + const encoded = ethers.concat([initializerHash, solidityPacked(['uint256'], [setupData.saltNonce])]) // Step 3: Hash the encoded value to get the final salt const salt = keccak256(encoded) // Get Proxy creation code - const proxyCreationCode = await memoizedGetProxyCreationCode(safeCreationData.factoryAddress, provider) + const proxyCreationCode = await memoizedGetProxyCreationCode(factoryAddress, provider) + + const initCode = proxyCreationCode + solidityPacked(['uint256'], [setupData.singleton]).slice(2) + return getCreate2Address(factoryAddress, salt, keccak256(initCode)) +} - const constructorData = safeCreationData.masterCopy - const initCode = proxyCreationCode + solidityPacked(['uint256'], [constructorData]).slice(2) - return getCreate2Address(safeCreationData.factoryAddress, salt, keccak256(initCode)) +export const predictAddressBasedOnReplayData = async (safeCreationData: ReplayedSafeProps, provider: Provider) => { + const initializer = encodeSafeSetupCall(safeCreationData.safeAccountConfig) + return predictSafeAddress( + { initializer, saltNonce: safeCreationData.saltNonce, singleton: safeCreationData.masterCopy }, + safeCreationData.factoryAddress, + provider, + ) } export const hasMultiChainCreationFeatures = (chain: ChainInfo): boolean => { diff --git a/src/hooks/__tests__/usePredictSafeAddressFromTxDetails.test.ts b/src/hooks/__tests__/usePredictSafeAddressFromTxDetails.test.ts new file mode 100644 index 0000000000..e1c13d7729 --- /dev/null +++ b/src/hooks/__tests__/usePredictSafeAddressFromTxDetails.test.ts @@ -0,0 +1,58 @@ +import type { DataDecoded } from '@safe-global/safe-gateway-typescript-sdk' +import { _getSetupFromDataDecoded } from '../usePredictSafeAddressFromTxDetails' + +const createProxyWithNonce = { + method: 'createProxyWithNonce', + parameters: [ + { + name: '_singleton', + type: 'address', + value: '0x41675C099F32341bf84BFc5382aF534df5C7461a', + valueDecoded: null, + }, + { + name: 'initializer', + type: 'bytes', + value: + '0xb63e800d00000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000bd89a1ce4dde368ffab0ec35506eece0b1ffdc540000000000000000000000000000000000000000000000000000000000000140000000000000000000000000fd0732dc9e303f09fcef3a7388ad10a83459ec99000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000057c26d4d117c926a872814fa46c179691f580e840000000000000000000000000000000000000000000000000000000000000024fe51f64300000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c76200000000000000000000000000000000000000000000000000000000', + valueDecoded: null, + }, + { + name: 'saltNonce', + type: 'uint256', + value: '3', + valueDecoded: null, + }, + ], +} as unknown as DataDecoded + +describe('getSetupFromDataDecoded', () => { + it('should return undefined if no createProxyWithNonce method is found', () => { + const dataDecoded = { + method: 'notCreateProxyWithNonce', + } + expect(_getSetupFromDataDecoded(dataDecoded)).toBeUndefined() + }) + + it('should return direct createProxyWithNonce calls', () => { + expect(_getSetupFromDataDecoded(createProxyWithNonce)).toEqual({ + singleton: '0x41675C099F32341bf84BFc5382aF534df5C7461a', + initializer: + '0xb63e800d00000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000bd89a1ce4dde368ffab0ec35506eece0b1ffdc540000000000000000000000000000000000000000000000000000000000000140000000000000000000000000fd0732dc9e303f09fcef3a7388ad10a83459ec99000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000057c26d4d117c926a872814fa46c179691f580e840000000000000000000000000000000000000000000000000000000000000024fe51f64300000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c76200000000000000000000000000000000000000000000000000000000', + saltNonce: '3', + }) + }) + + it.each([ + ['_singleton', 0], + ['initializer', 1], + ['saltNonce', 2], + ])('should return undefined if %s is not a string', (_, index) => { + const dataDecoded = JSON.parse(JSON.stringify(createProxyWithNonce)) as DataDecoded + // @ts-expect-error value is a string + dataDecoded.parameters[index].value = 1 + expect(_getSetupFromDataDecoded(dataDecoded)).toBeUndefined() + }) +}) + +it.todo('usePredictSafeAddressFromTxDetails') diff --git a/src/hooks/usePredictSafeAddressFromTxDetails.ts b/src/hooks/usePredictSafeAddressFromTxDetails.ts new file mode 100644 index 0000000000..fadc911048 --- /dev/null +++ b/src/hooks/usePredictSafeAddressFromTxDetails.ts @@ -0,0 +1,60 @@ +import type { DataDecoded, TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' +import { predictSafeAddress } from '@/features/multichain/utils/utils' +import useAsync from './useAsync' +import { useWeb3ReadOnly } from './wallets/web3' + +export function _getSetupFromDataDecoded(dataDecoded: DataDecoded) { + if (dataDecoded?.method !== 'createProxyWithNonce') { + return + } + + const singleton = dataDecoded?.parameters?.[0]?.value + const initializer = dataDecoded?.parameters?.[1]?.value + const saltNonce = dataDecoded?.parameters?.[2]?.value + + if (typeof singleton !== 'string' || typeof initializer !== 'string' || typeof saltNonce !== 'string') { + return + } + + return { + singleton, + initializer, + saltNonce, + } +} + +function isCreateProxyWithNonce(dataDecoded?: DataDecoded) { + return dataDecoded?.method === 'createProxyWithNonce' +} + +export function usePredictSafeAddressFromTxDetails(txDetails: TransactionDetails | undefined) { + const web3 = useWeb3ReadOnly() + + return useAsync(() => { + const txData = txDetails?.txData + if (!web3 || !txData) { + return + } + + const isMultiSend = txData?.dataDecoded?.method === 'multiSend' + + const dataDecoded = isMultiSend + ? txData?.dataDecoded?.parameters?.[0]?.valueDecoded?.find((tx) => isCreateProxyWithNonce(tx?.dataDecoded)) + ?.dataDecoded + : txData?.dataDecoded + const factoryAddress = isMultiSend + ? txData?.dataDecoded?.parameters?.[0]?.valueDecoded?.find((tx) => isCreateProxyWithNonce(tx?.dataDecoded))?.to + : txData?.to?.value + + if (!dataDecoded || !isCreateProxyWithNonce(dataDecoded) || !factoryAddress) { + return + } + + const setup = _getSetupFromDataDecoded(dataDecoded) + if (!setup) { + return + } + + return predictSafeAddress(setup, factoryAddress, web3) + }, [txDetails?.txData, web3]) +} diff --git a/src/services/analytics/events/subaccounts.ts b/src/services/analytics/events/subaccounts.ts index 5261af9a86..4a73f2e8d0 100644 --- a/src/services/analytics/events/subaccounts.ts +++ b/src/services/analytics/events/subaccounts.ts @@ -1,8 +1,12 @@ const SUBACCOUNTS_CATEGORY = 'subaccounts' export const SUBACCOUNT_EVENTS = { - OPEN: { - action: 'Open', + OPEN_LIST: { + action: 'Open Subaccount list', + category: SUBACCOUNTS_CATEGORY, + }, + OPEN_SUBACCOUNT: { + action: 'Open Subaccount', category: SUBACCOUNTS_CATEGORY, }, SHOW_ALL: { @@ -22,4 +26,5 @@ export const SUBACCOUNT_EVENTS = { export enum SUBACCOUNT_LABELS { header = 'header', sidebar = 'sidebar', + success_screen = 'success_screen', }