diff --git a/extension/.cspell.json b/extension/.cspell.json index 623eb5ed..dd9acbc3 100644 --- a/extension/.cspell.json +++ b/extension/.cspell.json @@ -45,6 +45,7 @@ "rdns", "reflexer", "refork", + "rpcs", "Samczun", "sepolia", "shazow", @@ -58,6 +59,7 @@ "toastify", "Uids", "UNWRAPPER", + "vnet", "walletconnect", "whatsabi", "xdai", diff --git a/extension/.pnp.cjs b/extension/.pnp.cjs index fd524777..6fe3503e 100755 --- a/extension/.pnp.cjs +++ b/extension/.pnp.cjs @@ -68,6 +68,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["eslint-plugin-react-hooks", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:4.6.2"],\ ["ethereum-blockies-base64", "npm:1.0.2"],\ ["ethers", "npm:5.7.2"],\ + ["ethers-multisend", "npm:3.1.0"],\ ["ethers-proxies", "npm:1.0.0"],\ ["events", "npm:3.3.0"],\ ["isomorphic-fetch", "npm:3.0.0"],\ @@ -81,7 +82,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["react-icons", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:4.12.0"],\ ["react-modal", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:3.16.1"],\ ["react-moment", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:1.1.3"],\ - ["react-multisend", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:2.1.0"],\ ["react-select", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:5.8.0"],\ ["react-toastify", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:9.1.3"],\ ["rimraf", "npm:3.0.2"],\ @@ -13664,34 +13664,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ - ["react-multisend", [\ - ["npm:2.1.0", {\ - "packageLocation": "./.yarn/cache/react-multisend-npm-2.1.0-5385f450b0-0603640ffe.zip/node_modules/react-multisend/",\ - "packageDependencies": [\ - ["react-multisend", "npm:2.1.0"]\ - ],\ - "linkType": "SOFT"\ - }],\ - ["virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:2.1.0", {\ - "packageLocation": "./.yarn/__virtual__/react-multisend-virtual-6293e73d21/0/cache/react-multisend-npm-2.1.0-5385f450b0-0603640ffe.zip/node_modules/react-multisend/",\ - "packageDependencies": [\ - ["react-multisend", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:2.1.0"],\ - ["@ethersproject/abi", "npm:5.7.0"],\ - ["@ethersproject/abstract-provider", "npm:5.7.0"],\ - ["@ethersproject/address", "npm:5.7.0"],\ - ["@ethersproject/bignumber", "npm:5.7.0"],\ - ["@types/react", "npm:18.3.3"],\ - ["ethers-multisend", "npm:3.1.0"],\ - ["ethers-proxies", "npm:1.0.0"],\ - ["react", "npm:18.3.1"]\ - ],\ - "packagePeers": [\ - "@types/react",\ - "react"\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["react-select", [\ ["npm:5.8.0", {\ "packageLocation": "./.yarn/cache/react-select-npm-5.8.0-468e0395bb-c8398cc0ae.zip/node_modules/react-select/",\ @@ -17222,6 +17194,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["eslint-plugin-react-hooks", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:4.6.2"],\ ["ethereum-blockies-base64", "npm:1.0.2"],\ ["ethers", "npm:5.7.2"],\ + ["ethers-multisend", "npm:3.1.0"],\ ["ethers-proxies", "npm:1.0.0"],\ ["events", "npm:3.3.0"],\ ["isomorphic-fetch", "npm:3.0.0"],\ @@ -17235,7 +17208,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["react-icons", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:4.12.0"],\ ["react-modal", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:3.16.1"],\ ["react-moment", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:1.1.3"],\ - ["react-multisend", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:2.1.0"],\ ["react-select", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:5.8.0"],\ ["react-toastify", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:9.1.3"],\ ["rimraf", "npm:3.0.2"],\ diff --git a/extension/.yarn/cache/react-multisend-npm-2.1.0-5385f450b0-0603640ffe.zip b/extension/.yarn/cache/react-multisend-npm-2.1.0-5385f450b0-0603640ffe.zip deleted file mode 100644 index b32970ea..00000000 Binary files a/extension/.yarn/cache/react-multisend-npm-2.1.0-5385f450b0-0603640ffe.zip and /dev/null differ diff --git a/extension/package.json b/extension/package.json index 6dac9c1e..c0878828 100644 --- a/extension/package.json +++ b/extension/package.json @@ -75,7 +75,6 @@ "react-icons": "^4.3.1", "react-modal": "^3.16.1", "react-moment": "^1.1.3", - "react-multisend": "^2.1.0", "react-select": "^5.2.1", "react-toastify": "^9.0.8", "rimraf": "^3.0.2", @@ -85,6 +84,7 @@ }, "packageManager": "yarn@3.7.0", "dependencies": { + "ethers-multisend": "^3.1.0", "zodiac-roles-deployments": "^2.2.2" } } diff --git a/extension/src/background.ts b/extension/src/background.ts index bbe45ff3..625e71d4 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -167,6 +167,18 @@ const removeRpcRedirectRules = (tabId: number) => { chrome.declarativeNetRequest.updateSessionRules({ removeRuleIds: ruleIds, }) + console.log( + 'removeRpcRedirectRules', + tabId, + ruleIds, + hash( + 'https://virtual.mainnet.rpc.tenderly.co/880388c4-9707-46ce-97a5-1095090a6768', + 735219801 + ) + ) + chrome.declarativeNetRequest.getSessionRules((rules) => + console.log('removeRpcRedirectRules getSessionRules', rules) + ) } chrome.runtime.onMessage.addListener((message, sender) => { @@ -174,6 +186,11 @@ chrome.runtime.onMessage.addListener((message, sender) => { if (message.type === 'startSimulating') { const { networkId, rpcUrl } = message + console.log('startSimulating', networkId, rpcUrl, { + simulatingExtensionTabs, + }) + simulatingExtensionTabs.delete(sender.tab.id) + removeRpcRedirectRules(sender.tab.id) simulatingExtensionTabs.set(sender.tab.id, { networkId, rpcUrl, @@ -186,6 +203,7 @@ chrome.runtime.onMessage.addListener((message, sender) => { ) } if (message.type === 'stopSimulating') { + console.log('stopSimulating', sender.tab.id, { simulatingExtensionTabs }) simulatingExtensionTabs.delete(sender.tab.id) removeRpcRedirectRules(sender.tab.id) diff --git a/extension/src/browser/Drawer/CallContract.tsx b/extension/src/browser/Drawer/CallContract.tsx deleted file mode 100644 index 73d55fb0..00000000 --- a/extension/src/browser/Drawer/CallContract.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react' -import { - CallContractTransactionInput, - NetworkId, - useContractCall, -} from 'react-multisend' - -import { Box } from '../../components' -import { useConnection } from '../../connections' - -import classes from './style.module.css' -import { EXPLORER_API_KEY } from '../../chains' - -interface Props { - value: CallContractTransactionInput -} - -const CallContract: React.FC = ({ value }) => { - const { - connection: { chainId }, - } = useConnection() - const { inputs } = useContractCall({ - value, - onChange: () => { - /*nothing here*/ - }, - network: chainId.toString() as NetworkId, - blockExplorerApiKey: EXPLORER_API_KEY[chainId], - }) - - return ( -
- {inputs.length > 0 && ( -
- {inputs.map((input) => ( - - ))} -
- )} -
- ) -} - -export default CallContract diff --git a/extension/src/browser/Drawer/ContractAddress/index.tsx b/extension/src/browser/Drawer/ContractAddress/index.tsx index c7503b67..9af85065 100644 --- a/extension/src/browser/Drawer/ContractAddress/index.tsx +++ b/extension/src/browser/Drawer/ContractAddress/index.tsx @@ -1,18 +1,18 @@ import copy from 'copy-to-clipboard' import makeBlockie from 'ethereum-blockies-base64' import { getAddress } from 'ethers/lib/utils' -import React, { useEffect, useMemo, useState } from 'react' +import React, { useMemo } from 'react' import { RiExternalLinkLine, RiFileCopyLine } from 'react-icons/ri' import { BlockLink, Box, Flex, IconButton } from '../../../components' import { EXPLORER_URL } from '../../../chains' import { useConnection } from '../../../connections' - import classes from './style.module.css' -import { fetchContractInfo } from '../../fetchContractInfo' +import { ContractInfo } from '../../../utils/abi' interface Props { address: string + contractInfo?: ContractInfo explorerLink?: boolean copyToClipboard?: boolean className?: string @@ -23,6 +23,7 @@ const VISIBLE_END = 4 const ContractAddress: React.FC = ({ address, + contractInfo, explorerLink, copyToClipboard, className, @@ -30,7 +31,7 @@ const ContractAddress: React.FC = ({ const { connection: { chainId }, } = useConnection() - const [contractName, setContractName] = useState('') + const explorerUrl = EXPLORER_URL[chainId] const blockie = useMemo(() => address && makeBlockie(address), [address]) @@ -40,20 +41,6 @@ const ContractAddress: React.FC = ({ const end = checksumAddress.substring(42 - VISIBLE_END, 42) const displayAddress = `${start}...${end}` - useEffect(() => { - let canceled = false - fetchContractInfo(address as `0x${string}`, chainId).then((info) => { - if (!canceled) { - setContractName(info.name || '') - } - }) - - return () => { - setContractName('') - canceled = true - } - }, [chainId, address]) - return ( = ({ {address} - {contractName && ( -
{contractName}
+ {contractInfo?.name && ( +
{contractInfo?.name}
)} diff --git a/extension/src/browser/Drawer/CopyToClipboard.tsx b/extension/src/browser/Drawer/CopyToClipboard.tsx index 85d79a97..1cfe903a 100644 --- a/extension/src/browser/Drawer/CopyToClipboard.tsx +++ b/extension/src/browser/Drawer/CopyToClipboard.tsx @@ -1,6 +1,6 @@ +import { MetaTransaction } from 'ethers-multisend' import React from 'react' import { RiFileCopy2Line } from 'react-icons/ri' -import { encodeSingle, TransactionInput } from 'react-multisend' import { toast } from 'react-toastify' import { IconButton } from '../../components' @@ -8,25 +8,13 @@ import { IconButton } from '../../components' import classes from './style.module.css' interface Props { - transaction: TransactionInput - isDelegateCall: boolean + transaction: MetaTransaction labeled?: boolean } -const CopyToClipboard: React.FC = ({ - transaction, - isDelegateCall, - labeled, -}) => { - const encodedTransaction = { - ...encodeSingle(transaction), - operation: isDelegateCall ? 1 : 0, - } - +const CopyToClipboard: React.FC = ({ transaction, labeled }) => { const copyToClipboard = () => { - navigator.clipboard.writeText( - JSON.stringify(encodedTransaction, undefined, 2) - ) + navigator.clipboard.writeText(JSON.stringify(transaction, undefined, 2)) toast(<>Transaction data has been copied to clipboard.) } diff --git a/extension/src/browser/Drawer/DecodedTransaction.tsx b/extension/src/browser/Drawer/DecodedTransaction.tsx new file mode 100644 index 00000000..51fa56b5 --- /dev/null +++ b/extension/src/browser/Drawer/DecodedTransaction.tsx @@ -0,0 +1,32 @@ +import React from 'react' + +import classes from './style.module.css' +import { Box } from '../../components' +import { FunctionFragment, Result } from '@ethersproject/abi' + +interface Props { + functionFragment: FunctionFragment + data: Result +} +const DecodedTransaction: React.FC = ({ functionFragment, data }) => { + return ( +
+ {functionFragment.inputs.length > 0 && ( +
+ {functionFragment.inputs.map((input, i) => ( + + ))} +
+ )} +
+ ) +} + +export default DecodedTransaction diff --git a/extension/src/browser/Drawer/RawTransaction.tsx b/extension/src/browser/Drawer/RawTransaction.tsx index ebe79873..17c75967 100644 --- a/extension/src/browser/Drawer/RawTransaction.tsx +++ b/extension/src/browser/Drawer/RawTransaction.tsx @@ -1,15 +1,14 @@ import React from 'react' -import { RawTransactionInput } from 'react-multisend' import { Box } from '../../components' import classes from './style.module.css' interface Props { - value: RawTransactionInput + data: string } -const RawTransaction: React.FC = ({ value }) => ( +const RawTransaction: React.FC = ({ data }) => (
- {input.type === TransactionType.callContract - ? input.functionSignature.split('(')[0] + {functionFragment + ? functionFragment.format('sighash').split('(')[0] : 'Raw transaction'} - {isDelegateCall && ( + {transactionState.transaction.operation === 1 && ( delegatecall )}
- {transactionHash && ( - - )} - + {showRoles && ( )} - - - - + + +
) } -interface BodyProps { - input: TransactionInput -} - -const TransactionBody: React.FC = ({ input }) => { - // const { network, blockExplorerApiKey } = useMultiSendContext() - let txInfo: ReactNode = <> - switch (input.type) { - case TransactionType.callContract: - txInfo = - break - // case TransactionType.transferFunds: - // return - // case TransactionType.transferCollectible: - // return - case TransactionType.raw: - txInfo = - break - } - return ( - - {txInfo} - - ) -} - -type Props = TransactionState & { +interface Props { + transactionState: TransactionState index: number scrollIntoView: boolean } export const Transaction: React.FC = ({ index, - transactionHash, - input, - isDelegateCall, + transactionState, scrollIntoView, }) => { const [expanded, setExpanded] = useState(true) const { connection } = useConnection() const elementRef = useScrollIntoView(scrollIntoView) + + const decoded = useDecodedFunctionData(transactionState) + const showRoles = (connection.moduleType === KnownContracts.ROLES_V1 || connection.moduleType === KnownContracts.ROLES_V2) && @@ -137,9 +99,8 @@ export const Transaction: React.FC = ({ setExpanded(!expanded)} showRoles={showRoles} @@ -154,21 +115,27 @@ export const Transaction: React.FC = ({ className={classes.transactionSubtitle} > - +
- + + + {decoded ? ( + + ) : ( + + )} + )} @@ -177,9 +144,7 @@ export const Transaction: React.FC = ({ export const TransactionBadge: React.FC = ({ index, - transactionHash, - input, - isDelegateCall, + transactionState, scrollIntoView, }) => { const { connection } = useConnection() @@ -201,14 +166,12 @@ export const TransactionBadge: React.FC = ({ rounded >
{index + 1}
- {transactionHash && ( - - )} + + {showRoles && ( @@ -217,15 +180,14 @@ export const TransactionBadge: React.FC = ({ ) } -interface StatusProps extends TransactionState { +interface StatusProps { + transactionState: TransactionState showRoles?: boolean index: number } const TransactionStatus: React.FC = ({ - input, - isDelegateCall, - transactionHash, + transactionState, index, showRoles = false, }) => ( @@ -235,16 +197,14 @@ const TransactionStatus: React.FC = ({ className={classes.transactionStatus} direction="column" > - {transactionHash && ( - - - - )} + + + + {showRoles && ( @@ -252,23 +212,12 @@ const TransactionStatus: React.FC = ({
) -const EtherValue: React.FC<{ input: TransactionInput }> = ({ input }) => { +const EtherValue: React.FC<{ value: string }> = ({ value }) => { const { connection: { chainId }, } = useConnection() - let value = '' - if ( - input.type === TransactionType.callContract || - input.type === TransactionType.raw - ) { - value = input.value - } - - if (!value) { - return null - } - const valueBN = BigNumber.from(value) + const valueBN = BigNumber.from(value || 0) return ( = ({ - transaction, - isDelegateCall, + transactionState, index, labeled, }) => { const provider = useProvider() const dispatch = useDispatch() const transactions = useNewTransactions() - const encodedTransaction = { - ...encodeSingle(transaction), - operation: isDelegateCall ? 1 : 0, - } - const translation = useApplicableTranslation(encodedTransaction) + const translation = useApplicableTranslation(transactionState.transaction) if (!(provider instanceof ForkProvider)) { // Transaction translation is only supported when using ForkProvider @@ -45,13 +37,16 @@ export const Translate: React.FC = ({ const handleTranslate = async () => { const laterTransactions = transactions .slice(index + 1) - .map(encodeTransaction) + .map((txState) => txState.transaction) // remove the transaction and all later ones from the store - dispatch({ type: 'REMOVE_TRANSACTION', payload: { id: transaction.id } }) + dispatch({ + type: 'REMOVE_TRANSACTION', + payload: { snapshotId: transactionState.snapshotId }, + }) // revert to checkpoint before the transaction to remove - const checkpoint = transaction.id // the ForkProvider uses checkpoints as IDs for the recorded transactions + const checkpoint = transactionState.transactionHash // the ForkProvider uses checkpoints as IDs for the recorded transactions await provider.request({ method: 'evm_revert', params: [checkpoint] }) // re-simulate all transactions starting with the translated ones diff --git a/extension/src/browser/Drawer/index.tsx b/extension/src/browser/Drawer/index.tsx index ffd8604c..210a75de 100644 --- a/extension/src/browser/Drawer/index.tsx +++ b/extension/src/browser/Drawer/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react' import { RiFileCopy2Line, RiRefreshLine } from 'react-icons/ri' -import { encodeMulti } from 'react-multisend' +import { encodeMulti } from 'ethers-multisend' import { toast } from 'react-toastify' import { BlockButton, Button, Drawer, Flex, IconButton } from '../../components' @@ -17,7 +17,6 @@ import { import Submit from './Submit' import { Transaction, TransactionBadge } from './Transaction' import classes from './style.module.css' -import { encodeTransaction } from '../../encodeTransaction' const TransactionsDrawer: React.FC = () => { const [expanded, setExpanded] = useState(true) @@ -46,7 +45,7 @@ const TransactionsDrawer: React.FC = () => { // remove all transactions from the store dispatch({ type: 'REMOVE_TRANSACTION', - payload: { id: allTransactions[0].input.id }, + payload: { snapshotId: allTransactions[0].snapshotId }, }) if (!(provider instanceof ForkProvider)) { @@ -57,14 +56,15 @@ const TransactionsDrawer: React.FC = () => { // re-simulate all new transactions (assuming the already submitted ones have already been mined on the fresh fork) for (const transaction of newTransactions) { - const encoded = encodeTransaction(transaction) - await provider.sendMetaTransaction(encoded) + await provider.sendMetaTransaction(transaction.transaction) } } const copyTransactionData = () => { if (!connection.chainId) throw new Error('chainId is undefined') - const metaTransactions = newTransactions.map(encodeTransaction) + const metaTransactions = newTransactions.map( + (txState) => txState.transaction + ) const batchTransaction = metaTransactions.length === 1 ? metaTransactions[0] @@ -99,9 +99,9 @@ const TransactionsDrawer: React.FC = () => { className={classes.body + ' coll'} direction="column" > - {newTransactions.map((transaction, index) => ( + {newTransactions.map((transactionState, index) => ( { ev.stopPropagation() setScrollItemIntoView(index) @@ -110,8 +110,8 @@ const TransactionsDrawer: React.FC = () => { > ))} @@ -154,12 +154,12 @@ const TransactionsDrawer: React.FC = () => { className={classes.body + ' exp'} direction="column" > - {newTransactions.map((transaction, index) => ( + {newTransactions.map((transactionState, index) => ( ))} diff --git a/extension/src/browser/Drawer/useDecodedFunctionData.tsx b/extension/src/browser/Drawer/useDecodedFunctionData.tsx new file mode 100644 index 00000000..d329241d --- /dev/null +++ b/extension/src/browser/Drawer/useDecodedFunctionData.tsx @@ -0,0 +1,33 @@ +import { Interface } from '@ethersproject/abi' +import { useMemo } from 'react' +import { TransactionState } from '../../state' + +export const useDecodedFunctionData = (transactionState: TransactionState) => { + const { contractInfo, transaction } = transactionState + const abi = contractInfo?.abi + + return useMemo(() => { + if (!abi) return null + + const selector = transaction.data.slice(0, 10) + if (selector.length !== 10) { + return null + } + + let contractInterface: Interface + try { + contractInterface = new Interface(abi) + const functionFragment = contractInterface.getFunction(selector) + return { + functionFragment, + data: contractInterface.decodeFunctionData( + functionFragment, + transaction.data + ), + } + } catch (e) { + console.error('Error decoding using ABI', e, { selector, abi }) + return null + } + }, [abi, transaction]) +} diff --git a/extension/src/browser/ProvideProvider.tsx b/extension/src/browser/ProvideProvider.tsx index 840b1806..d9ca818b 100644 --- a/extension/src/browser/ProvideProvider.tsx +++ b/extension/src/browser/ProvideProvider.tsx @@ -1,4 +1,3 @@ -import { Web3Provider } from '@ethersproject/providers' import React, { createContext, ReactNode, @@ -6,7 +5,7 @@ import React, { useContext, useMemo, } from 'react' -import { decodeSingle, encodeMulti } from 'react-multisend' +import { encodeMulti } from 'ethers-multisend' import { ForkProvider, @@ -14,11 +13,12 @@ import { WrappingProvider, } from '../providers' import { useConnection } from '../connections' -import { Eip1193Provider } from '../types' - -import { fetchContractInfo } from './fetchContractInfo' +import { Connection, Eip1193Provider } from '../types' import { useDispatch, useNewTransactions } from '../state' -import { encodeTransaction } from '../encodeTransaction' +import { fetchContractInfo } from '../utils/abi' +import { TransactionReceipt, Web3Provider } from '@ethersproject/providers' +import { ExecutionStatus } from '../state/reducer' +import { defaultAbiCoder } from '@ethersproject/abi' interface Props { simulate: boolean @@ -52,56 +52,77 @@ const ProvideProvider: React.FC = ({ simulate, children }) => { moduleAddress: connection.moduleAddress, ownerAddress: connection.pilotAddress, - async onBeforeTransactionSend(txId, metaTx) { - const isDelegateCall = metaTx.operation === 1 - - // Calling decodeSingle without a fetchAbi will return a raw transaction input object instantly. - // We already append to the state so the UI reacts immediately. - const inputRaw = await decodeSingle( - metaTx, - new Web3Provider(provider), - undefined, - txId - ) + async onBeforeTransactionSend(snapshotId, transaction) { + // Immediately update the state with the transaction so that the UI can show it as pending. dispatch({ - type: 'APPEND_RAW_TRANSACTION', - payload: { input: inputRaw, isDelegateCall }, + type: 'APPEND_TRANSACTION', + payload: { transaction, snapshotId }, }) - // Now we can take some time decoding the transaction for real and we update the state once that's done. - const input = await decodeSingle( - metaTx, - new Web3Provider(provider), - async (address: string) => { - const info = await fetchContractInfo( - address as `0x${string}`, - connection.chainId - ) - return JSON.stringify(info.abi) - }, - txId + // Now we can take some time decoding the transaction and we update the state once that's done. + const contractInfo = await fetchContractInfo( + transaction.to as `0x${string}`, + connection.chainId ) dispatch({ type: 'DECODE_TRANSACTION', - payload: input, + payload: { + snapshotId, + contractInfo, + }, }) }, - async onTransactionSent(txId, transactionHash) { + async onTransactionSent(snapshotId, transactionHash) { dispatch({ type: 'CONFIRM_TRANSACTION', payload: { - id: txId, + snapshotId, transactionHash, }, }) + + const receipt = await new Web3Provider( + tenderlyProvider + ).getTransactionReceipt(transactionHash) + if (!receipt.status) { + dispatch({ + type: 'UPDATE_TRANSACTION_STATUS', + payload: { + snapshotId, + status: ExecutionStatus.REVERTED, + }, + }) + return + } + + if ( + receipt.logs.length === 1 && + isExecutionFromModuleFailure(receipt.logs[0], connection) + ) { + dispatch({ + type: 'UPDATE_TRANSACTION_STATUS', + payload: { + snapshotId, + status: ExecutionStatus.MODULE_TRANSACTION_REVERTED, + }, + }) + } else { + dispatch({ + type: 'UPDATE_TRANSACTION_STATUS', + payload: { + snapshotId, + status: ExecutionStatus.SUCCESS, + }, + }) + } }, }), - [tenderlyProvider, provider, connection, dispatch] + [tenderlyProvider, connection, dispatch] ) const submitTransactions = useCallback(async () => { - const metaTransactions = transactions.map(encodeTransaction) + const metaTransactions = transactions.map((txState) => txState.transaction) console.log( transactions.length === 1 @@ -142,3 +163,16 @@ const ProvideProvider: React.FC = ({ simulate, children }) => { } export default ProvideProvider + +const isExecutionFromModuleFailure = ( + log: TransactionReceipt['logs'][0], + connection: Connection +) => { + return ( + log.address.toLowerCase() === connection.avatarAddress.toLowerCase() && + log.topics[0] === + '0xacd2c8702804128fdb0db2bb49f6d127dd0181c13fd45dbfe16de0930e2bd375' && // ExecutionFromModuleFailure(address) + log.topics[1] === + defaultAbiCoder.encode(['address'], [connection.moduleAddress]) + ) +} diff --git a/extension/src/encodeTransaction.ts b/extension/src/encodeTransaction.ts deleted file mode 100644 index 0b82b839..00000000 --- a/extension/src/encodeTransaction.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { encodeSingle } from 'react-multisend' -import { TransactionState } from './state' - -export const encodeTransaction = (transaction: TransactionState) => { - return { - ...encodeSingle(transaction.input), - operation: transaction.isDelegateCall ? 1 : 0, - } -} diff --git a/extension/src/integrations/safe/sendTransaction.ts b/extension/src/integrations/safe/sendTransaction.ts index d2f22635..4eb4fed1 100644 --- a/extension/src/integrations/safe/sendTransaction.ts +++ b/extension/src/integrations/safe/sendTransaction.ts @@ -1,11 +1,10 @@ import Safe, { EthersAdapter } from '@safe-global/protocol-kit' import * as ethers from 'ethers' +import { MetaTransaction } from 'ethers-multisend' import { getAddress } from 'ethers/lib/utils' -import { MetaTransaction } from 'react-multisend' -import { getReadOnlyProvider } from '../../providers/readOnlyProvider' +import { getReadOnlyProvider } from '../../providers/readOnlyProvider' import { Connection, Eip1193Provider, TransactionData } from '../../types' - import { initSafeApiKit } from './kits' import { waitForMultisigExecution } from './waitForMultisigExecution' diff --git a/extension/src/integrations/safe/signing.ts b/extension/src/integrations/safe/signing.ts index 65041b04..57b69493 100644 --- a/extension/src/integrations/safe/signing.ts +++ b/extension/src/integrations/safe/signing.ts @@ -1,7 +1,7 @@ import { EIP712TypedData } from '@safe-global/safe-gateway-typescript-sdk' import { Contract } from 'ethers' +import { MetaTransaction } from 'ethers-multisend' import { hashMessage, _TypedDataEncoder, toUtf8String } from 'ethers/lib/utils' -import { MetaTransaction } from 'react-multisend' const SIGN_MESSAGE_LIB_ADDRESS = '0xd53cd0aB83D845Ac265BE939c57F53AD838012c9' const SIGN_MESSAGE_LIB_ABI = [ diff --git a/extension/src/providers/ForkProvider.ts b/extension/src/providers/ForkProvider.ts index dc36aca7..949c9ba8 100644 --- a/extension/src/providers/ForkProvider.ts +++ b/extension/src/providers/ForkProvider.ts @@ -2,9 +2,9 @@ import EventEmitter from 'events' import { ContractFactories, KnownContracts } from '@gnosis.pm/zodiac' import { BigNumber, ethers } from 'ethers' -import { MetaTransaction } from 'react-multisend' import { TransactionOptions } from '@safe-global/safe-core-sdk-types' import { generatePreValidatedSignature } from '@safe-global/protocol-kit/dist/src/utils' +import { MetaTransaction } from 'ethers-multisend' import { Eip1193Provider, TransactionData } from '../types' import { TenderlyProvider } from './ProvideTenderly' @@ -194,7 +194,7 @@ class ForkProvider extends EventEmitter { tx = { to: metaTx.to, data: metaTx.data, - value: formatValue(metaTx.value), + value: formatHexValue(metaTx.value), from: this.avatarAddress, } } @@ -220,7 +220,7 @@ class ForkProvider extends EventEmitter { export default ForkProvider // Tenderly has particular requirements for the encoding of value: it must not have any leading zeros -const formatValue = (value: string): string => { +const formatHexValue = (value: string): string => { const valueBN = BigNumber.from(value) if (valueBN.isZero()) return '0x0' else return valueBN.toHexString().replace(/^0x(0+)/, '0x') @@ -249,9 +249,9 @@ const execTransactionFromModule = ( value: '0x0', from: moduleAddress, // We simulate setting the entire block gas limit as the gas limit for the transaction - gasLimit: hexlify(blockGasLimit), + gas: formatHexValue(hexlify(blockGasLimit)), // Tenderly errors if the hex value has leading zeros // With gas price 0 account don't need token for gas - gasPrice: '0x0', + // gasPrice: '0x0', // doesn't seem to be required } } @@ -285,9 +285,9 @@ export function execTransaction( value: '0x0', from: ownerAddress, // We simulate setting the entire block gas limit as the gas limit for the transaction - gasLimit: hexlify(blockGasLimit), + gas: hexlify(blockGasLimit / 2), // for some reason tenderly errors when passing the full block gas limit // With gas price 0 account don't need token for gas - gasPrice: '0x0', + // gasPrice: '0x0', // doesn't seem to be required } } diff --git a/extension/src/providers/ProvideTenderly.tsx b/extension/src/providers/ProvideTenderly.tsx index af9492c6..9ce9cc66 100644 --- a/extension/src/providers/ProvideTenderly.tsx +++ b/extension/src/providers/ProvideTenderly.tsx @@ -2,6 +2,7 @@ import EventEmitter from 'events' import { JsonRpcProvider } from '@ethersproject/providers' import React, { useContext, useEffect, useMemo } from 'react' +import { customAlphabet } from 'nanoid' import { useConnection } from '../connections' import { Eip1193Provider, JsonRpcRequest } from '../types' @@ -11,6 +12,8 @@ import { safeInterface } from '../integrations/safe' import { getEip1193ReadOnlyProvider } from './readOnlyProvider' import { ChainId } from '../chains' +const slug = customAlphabet('abcdefghijklmnopqrstuvwxyz0123456789') + const TenderlyContext = React.createContext(null) export const useTenderlyProvider = (): TenderlyProvider => { @@ -121,71 +124,23 @@ async function prepareSafeForSimulation( } } -export interface TenderlyTransactionInfo { - id: string - project_id: string - dashboardLink: string - fork_id: string - hash: string - block_number: number - gas: number - queue_origin: string - gas_price: string - value: string - status: boolean - fork_height: number - block_hash: string - nonce: number - receipt: { - transactionHash: string - transactionIndex: string - blockHash: string - blockNumber: string - from: string - to: string - cumulativeGasUsed: string - gasUsed: string - effectiveGasPrice: string - contractAddress: null - logs: { - logIndex: string - address: string - topics: string[] - data: string - blockHash: string - blockNumber: string - removed: boolean - transactionHash: string - transactionIndex: string - }[] - logsBloom: string - status: string - type: string - } - parent_id: string - created_at: string - timestamp: string -} - export class TenderlyProvider extends EventEmitter { private provider: Eip1193Provider private chainId: number private forkProviderPromise: Promise | undefined - private forkId: string | undefined - private transactionIds: Map = new Map() - private transactionInfo: Map> = - new Map() + private vnetId: string | undefined + private publicRpcSlug: string | undefined private blockNumber: number | undefined - private tenderlyForkApi: string + private tenderlyVnetApi: string private throttledIncreaseBlock: () => void constructor(chainId: ChainId) { super() this.provider = getEip1193ReadOnlyProvider(chainId, ZERO_ADDRESS) this.chainId = chainId - this.tenderlyForkApi = 'https://fork-api.pilot.gnosisguild.org' + this.tenderlyVnetApi = 'https://vnet-api.pilot.gnosisguild.org' this.throttledIncreaseBlock = throttle(this.increaseBlock, 1000) } @@ -223,7 +178,7 @@ export class TenderlyProvider extends EventEmitter { } catch (e) { if ((e as any).error?.code === -32603) { console.error( - 'Tenderly fork RPC has an issue (probably due to rate limiting)', + 'Tenderly vnet RPC has an issue (probably due to rate limiting)', e ) throw new Error('Error sending request to Tenderly') @@ -233,108 +188,99 @@ export class TenderlyProvider extends EventEmitter { } if (request.method === 'eth_sendTransaction') { - // when sending a transaction, we need to retrieve that transaction's ID on Tenderly - const { global_head: headTransactionId, block_number } = - await this.fetchForkInfo() - this.blockNumber = block_number - this.transactionIds.set(result, headTransactionId) // result is the transaction hash + if (this.blockNumber) this.blockNumber++ } return result } - async getTransactionInfo( - transactionHash: string - ): Promise { - if (!this.transactionInfo.has(transactionHash)) { - this.transactionInfo.set( - transactionHash, - this.fetchTransactionInfo(transactionHash) - ) - } - - const transactionInfoPromise = this.transactionInfo.get(transactionHash) - if (!transactionInfoPromise) throw new Error('invariant violation') - - return await transactionInfoPromise - } - async refork() { this.deleteFork() - this.transactionInfo.clear() this.forkProviderPromise = this.createFork(this.chainId) return await this.forkProviderPromise } async deleteFork() { - await this.forkProviderPromise - if (!this.forkId) return - // notify the background script to stop intercepting JSON RPC requests window.postMessage({ type: 'stopSimulating', toBackground: true }, '*') - const forkId = this.forkId - this.forkId = undefined + await this.forkProviderPromise + if (!this.vnetId) return + + this.vnetId = undefined + this.publicRpcSlug = undefined this.forkProviderPromise = undefined this.blockNumber = undefined - await fetch(`${this.tenderlyForkApi}/${forkId}`, { - method: 'DELETE', - }) + + // We no longer delete forks/virtual testnets on Tenderly. That way we will be able to persist and share Pilot sessions in the future. + // (Also Tenderly doesn't seem to offer a DELETE endpoint for virtual networks.) + // await fetch(`${this.tenderlyVnetApi}/${vnetId}`, { + // method: 'DELETE', + // }) + } + + getTransactionLink(txHash: string) { + return `https://dashboard.tenderly.co/explorer/vnet/${this.publicRpcSlug}/tx/${txHash}` } private async createFork( networkId: number, blockNumber?: number ): Promise { - const res = await fetch(this.tenderlyForkApi, { + const res = await fetch(this.tenderlyVnetApi, { method: 'POST', body: JSON.stringify({ - network_id: networkId.toString(), - block_number: blockNumber, + slug: slug(), + display_name: 'Zodiac Pilot Test Flight', + fork_config: { + network_id: networkId, + block_number: + blockNumber || + (await this.provider.request({ + method: 'eth_blockNumber', + })), + }, + virtual_network_config: { + base_fee_per_gas: 0, + chain_config: { + chain_id: networkId, + }, + }, + sync_state_config: { + enabled: true, + }, + explorer_page_config: { + enabled: true, // enable public explorer page + verification_visibility: 'bytecode', + }, }), }) const json = await res.json() - this.forkId = json.simulation_fork.id - this.blockNumber = json.simulation_fork.block_number - this.transactionIds.clear() - const rpcUrl = `https://rpc.tenderly.co/fork/${this.forkId}` + this.vnetId = json.id + this.blockNumber = json.fork_config.block_number + + const adminRpc = json.rpcs.find((rpc: any) => rpc.name === 'Admin RPC').url + const publicRpc = json.rpcs.find( + (rpc: any) => rpc.name === 'Public RPC' + ).url + this.publicRpcSlug = publicRpc.split('/').pop() // notify the background script to start intercepting JSON RPC requests + // we use the public RPC for requests originating from apps window.postMessage( - { type: 'startSimulating', toBackground: true, networkId, rpcUrl }, + { + type: 'startSimulating', + toBackground: true, + networkId, + rpcUrl: publicRpc, + }, '*' ) - return new JsonRpcProvider(rpcUrl) - } - - private async fetchForkInfo() { - await this.forkProviderPromise - if (!this.forkId) throw new Error('No Tenderly fork available') - - const res = await fetch(`${this.tenderlyForkApi}/${this.forkId}`) - const json = await res.json() - return json.simulation_fork - } - - private async fetchTransactionInfo( - transactionHash: string - ): Promise { - if (!this.forkId) throw new Error('No Tenderly fork available') - - const transactionId = this.transactionIds.get(transactionHash) - if (!transactionId) throw new Error('Transaction not found') - - const res = await fetch( - `${this.tenderlyForkApi}/${this.forkId}/transaction/${transactionId}` - ) - const json = await res.json() - return { - ...json.fork_transaction, - dashboardLink: `https://dashboard.tenderly.co/public/gnosisguild/zodiac-pilot/fork-simulation/${transactionId}`, - } + // for requests going directly to Tenderly provider we use the admin RPC so Pilot can fully control the fork + return new JsonRpcProvider(adminRpc) } private increaseBlock = async () => { diff --git a/extension/src/providers/WrappingProvider.ts b/extension/src/providers/WrappingProvider.ts index a7b1129d..50832768 100644 --- a/extension/src/providers/WrappingProvider.ts +++ b/extension/src/providers/WrappingProvider.ts @@ -2,7 +2,7 @@ import EventEmitter from 'events' import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers' import { ContractFactories, KnownContracts } from '@gnosis.pm/zodiac' -import { MetaTransaction } from 'react-multisend' +import { MetaTransaction } from 'ethers-multisend' import { initSafeApiKit, sendTransaction } from '../integrations/safe' import { Connection, Eip1193Provider, TransactionData } from '../types' @@ -28,7 +28,7 @@ export function wrapRequest( data = RolesV1Interface.encodeFunctionData('execTransactionWithRole', [ request.to || '', request.value || 0, - request.data || '0x00', + request.data || '0x', ('operation' in request && request.operation) || 0, connection.roleId || 0, revertOnError, @@ -38,7 +38,7 @@ export function wrapRequest( data = RolesV2Interface.encodeFunctionData('execTransactionWithRole', [ request.to || '', request.value || 0, - request.data || '0x00', + request.data || '0x', ('operation' in request && request.operation) || 0, connection.roleId || '0x0000000000000000000000000000000000000000000000000000000000000000', @@ -49,7 +49,7 @@ export function wrapRequest( data = DelayInterface.encodeFunctionData('execTransactionFromModule', [ request.to || '', request.value || 0, - request.data || '0x00', + request.data || '0x', ('operation' in request && request.operation) || 0, ]) break diff --git a/extension/src/state/actions.ts b/extension/src/state/actions.ts index 9048c792..4a90f0ef 100644 --- a/extension/src/state/actions.ts +++ b/extension/src/state/actions.ts @@ -1,33 +1,50 @@ -import { TransactionInput } from 'react-multisend' +import { MetaTransaction } from 'ethers-multisend' +import { ContractInfo } from '../utils/abi' +import { ExecutionStatus } from './reducer' -interface AppendRawTransactionAction { - type: 'APPEND_RAW_TRANSACTION' - payload: { input: TransactionInput; isDelegateCall: boolean } +interface AppendTransactionAction { + type: 'APPEND_TRANSACTION' + payload: { + snapshotId: string + transaction: MetaTransaction + } } + interface DecodeTransactionAction { type: 'DECODE_TRANSACTION' - payload: TransactionInput + payload: { + snapshotId: string + contractInfo: ContractInfo + } } interface ConfirmTransactionAction { type: 'CONFIRM_TRANSACTION' payload: { - id: string + snapshotId: string transactionHash: string } } +interface UpdateTransactionStatusAction { + type: 'UPDATE_TRANSACTION_STATUS' + payload: { + snapshotId: string + status: ExecutionStatus + } +} + interface RemoveTransactionAction { type: 'REMOVE_TRANSACTION' payload: { - id: string + snapshotId: string } } interface RemoveTransactionAction { type: 'REMOVE_TRANSACTION' payload: { - id: string + snapshotId: string } } @@ -46,9 +63,10 @@ interface ClearTransactionsAction { } export type Action = - | AppendRawTransactionAction + | AppendTransactionAction | DecodeTransactionAction | ConfirmTransactionAction + | UpdateTransactionStatusAction | RemoveTransactionAction | SubmitTransactionsAction | ClearTransactionsAction diff --git a/extension/src/state/reducer.ts b/extension/src/state/reducer.ts index ed4171b8..4609a6f5 100644 --- a/extension/src/state/reducer.ts +++ b/extension/src/state/reducer.ts @@ -1,10 +1,19 @@ -import { TransactionInput } from 'react-multisend' - +import { MetaTransaction } from 'ethers-multisend' +import { ContractInfo } from '../utils/abi' import { Action } from './actions' +export enum ExecutionStatus { + PENDING, + SUCCESS, + REVERTED, + MODULE_TRANSACTION_REVERTED, +} + export interface TransactionState { - input: TransactionInput - isDelegateCall: boolean + snapshotId: string + transaction: MetaTransaction + status: ExecutionStatus + contractInfo?: ContractInfo transactionHash?: string batchTransactionHash?: string } @@ -14,30 +23,40 @@ const rootReducer = ( action: Action ): TransactionState[] => { switch (action.type) { - case 'APPEND_RAW_TRANSACTION': { - const { input, isDelegateCall } = action.payload - return [...state, { input, isDelegateCall }] + case 'APPEND_TRANSACTION': { + const { snapshotId, transaction } = action.payload + return [ + ...state, + { snapshotId, transaction, status: ExecutionStatus.PENDING }, + ] } case 'DECODE_TRANSACTION': { - const input = action.payload + const { snapshotId, contractInfo } = action.payload return state.map((item) => - item.input.id === input.id ? { ...item, input } : item + item.snapshotId === snapshotId ? { ...item, contractInfo } : item ) } case 'CONFIRM_TRANSACTION': { - const { id, transactionHash } = action.payload + const { snapshotId, transactionHash } = action.payload + return state.map((item) => + item.snapshotId === snapshotId ? { ...item, transactionHash } : item + ) + } + + case 'UPDATE_TRANSACTION_STATUS': { + const { snapshotId, status } = action.payload return state.map((item) => - item.input.id === id ? { ...item, transactionHash } : item + item.snapshotId === snapshotId ? { ...item, status } : item ) } case 'REMOVE_TRANSACTION': { - const { id } = action.payload + const { snapshotId } = action.payload return state.slice( 0, - state.findIndex((item) => item.input.id === id) + state.findIndex((item) => item.snapshotId === snapshotId) ) } diff --git a/extension/src/state/transactionHooks.ts b/extension/src/state/transactionHooks.ts index 36f60fed..2d1fe417 100644 --- a/extension/src/state/transactionHooks.ts +++ b/extension/src/state/transactionHooks.ts @@ -18,7 +18,7 @@ export const useClearTransactions = () => { dispatch({ type: 'REMOVE_TRANSACTION', - payload: { id: transactions[0].input.id }, + payload: { snapshotId: transactions[0].snapshotId }, }) if (provider instanceof ForkProvider) { diff --git a/extension/src/transactionTranslations/index.ts b/extension/src/transactionTranslations/index.ts index 2041ec54..bca25c30 100644 --- a/extension/src/transactionTranslations/index.ts +++ b/extension/src/transactionTranslations/index.ts @@ -1,5 +1,5 @@ +import { MetaTransaction } from 'ethers-multisend' import { useEffect, useState } from 'react' -import { MetaTransaction } from 'react-multisend' import { ChainId } from '../chains' import { useConnection } from '../connections' diff --git a/extension/src/transactionTranslations/signSnapshotVote.ts b/extension/src/transactionTranslations/signSnapshotVote.ts index 186b6cc9..aa929e30 100644 --- a/extension/src/transactionTranslations/signSnapshotVote.ts +++ b/extension/src/transactionTranslations/signSnapshotVote.ts @@ -1,5 +1,5 @@ import { Interface } from '@ethersproject/abi' -import { MetaTransaction } from 'react-multisend' +import { MetaTransaction } from 'ethers-multisend' // https://github.com/gnosisguild/snapshot-signer const SNAPSHOT_SIGNER_ADDRESS = '0xb0382209806345d27dfdab5bbc17b2ab553165ac' diff --git a/extension/src/transactionTranslations/types.ts b/extension/src/transactionTranslations/types.ts index dc8c1c08..11e36588 100644 --- a/extension/src/transactionTranslations/types.ts +++ b/extension/src/transactionTranslations/types.ts @@ -1,5 +1,4 @@ -import { MetaTransaction } from 'react-multisend' - +import { MetaTransaction } from 'ethers-multisend' import { ChainId } from '../chains' import { SupportedModuleType } from '../integrations/zodiac/types' diff --git a/extension/src/browser/fetchContractInfo.ts b/extension/src/utils/abi.ts similarity index 100% rename from extension/src/browser/fetchContractInfo.ts rename to extension/src/utils/abi.ts diff --git a/extension/src/utils/decodeError.ts b/extension/src/utils/decodeError.ts index 4ff11ee8..8aec46e0 100644 --- a/extension/src/utils/decodeError.ts +++ b/extension/src/utils/decodeError.ts @@ -12,7 +12,7 @@ const RolesV2Interface = export function getRevertData(error: JsonRpcError) { // The errors thrown when a transaction is reverted use different formats, depending on: // - wallet (MetaMask vs. WalletConnect) - // - RPC provider (Infura vs. Alchemy) + // - RPC provider (Infura vs. Alchemy vs. Tenderly) // - client library (ethers vs. directly using the EIP-1193 provider) // first, drill through potential error wrappings down to the original error @@ -22,11 +22,13 @@ export function getRevertData(error: JsonRpcError) { // Here we try to extract the revert reason in any of the possible formats const message = - error.data?.originalError?.data || - error.data?.data || - error.data?.originalError?.message || - error.data?.message || - error.message + typeof error.data === 'string' + ? error.data + : error.data?.originalError?.data || + error.data?.data || + error.data?.originalError?.message || + error.data?.message || + error.message const prefix = 'Reverted 0x' return message.startsWith(prefix) @@ -55,6 +57,7 @@ export function decodeGenericError(error: JsonRpcError) { export function decodeRolesV1Error(error: JsonRpcError) { const revertData = getRevertData(error) + if (revertData.startsWith('0x')) { const rolesError = Object.values(RolesV1Interface.errors).find((err) => revertData.startsWith(RolesV1Interface.getSighash(err)) diff --git a/extension/yarn.lock b/extension/yarn.lock index 169ae268..ca2f4154 100644 --- a/extension/yarn.lock +++ b/extension/yarn.lock @@ -6458,7 +6458,7 @@ __metadata: languageName: node linkType: hard -"ethers-multisend@npm:^3.0.0": +"ethers-multisend@npm:^3.1.0": version: 3.1.0 resolution: "ethers-multisend@npm:3.1.0" dependencies: @@ -11446,22 +11446,6 @@ __metadata: languageName: node linkType: hard -"react-multisend@npm:^2.1.0": - version: 2.1.0 - resolution: "react-multisend@npm:2.1.0" - dependencies: - "@ethersproject/abi": ^5.0.0 - "@ethersproject/abstract-provider": ^5.5.1 - "@ethersproject/address": ^5.0.0 - "@ethersproject/bignumber": ^5.0.0 - ethers-multisend: ^3.0.0 - ethers-proxies: ^1.0.0 - peerDependencies: - react: ">= 16.8" - checksum: 0603640ffe7b8830542ada124a13277008e5c06ed3e68dba2ca3e5a8f13859da6c468aad5eac0cc2286ca1af64984051c08677ba15b5971bcb48d0bc32394665 - languageName: node - linkType: hard - "react-select@npm:^5.2.1": version: 5.8.0 resolution: "react-select@npm:5.8.0" @@ -14547,6 +14531,7 @@ __metadata: eslint-plugin-react-hooks: ^4.6.0 ethereum-blockies-base64: ^1.0.2 ethers: ^5.7.2 + ethers-multisend: ^3.1.0 ethers-proxies: ^1.0.0 events: ^3.3.0 isomorphic-fetch: ^3.0.0 @@ -14560,7 +14545,6 @@ __metadata: react-icons: ^4.3.1 react-modal: ^3.16.1 react-moment: ^1.1.3 - react-multisend: ^2.1.0 react-select: ^5.2.1 react-toastify: ^9.0.8 rimraf: ^3.0.2