diff --git a/extension/.pnp.cjs b/extension/.pnp.cjs index ce5f0091..1e8128c5 100755 --- a/extension/.pnp.cjs +++ b/extension/.pnp.cjs @@ -32,11 +32,11 @@ const RAW_RUNTIME_STATE = ["@eslint/js", "npm:9.7.0"],\ ["@gnosis.pm/zodiac", "npm:4.0.3"],\ ["@noble/hashes", "npm:1.4.0"],\ - ["@safe-global/api-kit", "npm:2.4.2"],\ - ["@safe-global/protocol-kit", "npm:4.0.2"],\ + ["@safe-global/api-kit", "npm:2.4.4"],\ + ["@safe-global/protocol-kit", "npm:4.0.4"],\ ["@safe-global/safe-apps-sdk", "npm:9.1.0"],\ - ["@safe-global/safe-core-sdk-types", "npm:5.0.2"],\ - ["@safe-global/safe-gateway-typescript-sdk", "npm:3.21.10"],\ + ["@safe-global/safe-core-sdk-types", "npm:5.0.3"],\ + ["@safe-global/safe-gateway-typescript-sdk", "npm:3.22.2"],\ ["@shazow/whatsabi", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:0.13.2"],\ ["@testing-library/dom", "npm:10.3.1"],\ ["@testing-library/jest-dom", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:6.4.6"],\ @@ -2222,6 +2222,17 @@ const RAW_RUNTIME_STATE = ["node-fetch", "virtual:9c67668db478e95ba4d6a763bc55027eeff0d22eaf59478017ea07386fc33a3c7b7b625af78aa86a33991a9a500a7aa216e28632de568f02adefd662ef53a42d#npm:2.7.0"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:2.4.4", {\ + "packageLocation": "./.yarn/cache/@safe-global-api-kit-npm-2.4.4-246439c931-578d1632bf.zip/node_modules/@safe-global/api-kit/",\ + "packageDependencies": [\ + ["@safe-global/api-kit", "npm:2.4.4"],\ + ["@safe-global/protocol-kit", "npm:4.0.4"],\ + ["@safe-global/safe-core-sdk-types", "npm:5.0.3"],\ + ["ethers", "npm:6.13.1"],\ + ["node-fetch", "virtual:9c67668db478e95ba4d6a763bc55027eeff0d22eaf59478017ea07386fc33a3c7b7b625af78aa86a33991a9a500a7aa216e28632de568f02adefd662ef53a42d#npm:2.7.0"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@safe-global/protocol-kit", [\ @@ -2236,7 +2247,27 @@ const RAW_RUNTIME_STATE = ["@safe-global/safe-core-sdk-types", "npm:5.0.2"],\ ["@safe-global/safe-deployments", "npm:1.37.1"],\ ["@types/bn.js", "npm:5.1.5"],\ - ["abitype", "virtual:e1ac12735a1a6142b16b0c18f4d069abadf688d06e8edb9c4971f939fb65c057d55015e07cf7a945ed65665e1980beae2649ffe7b99d5d39226d5b65db5bc1b2#npm:1.0.5"],\ + ["abitype", "virtual:50c0bb616918b71f9b7ad70bc29d6254b0be2a953e234cac59060d581c642cd881294af5c8ec28e7221aa8e14bc421b0626c2976b086cfd2795d2503310b14da#npm:1.0.5"],\ + ["bn.js", "npm:5.2.1"],\ + ["ethereumjs-util", "npm:7.1.5"],\ + ["ethers", "npm:6.13.1"],\ + ["semver", "npm:7.6.2"],\ + ["web3-eth-contract", "npm:4.5.0"]\ + ],\ + "linkType": "HARD"\ + }],\ + ["npm:4.0.4", {\ + "packageLocation": "./.yarn/cache/@safe-global-protocol-kit-npm-4.0.4-35473a7fe1-4acdfa6d2c.zip/node_modules/@safe-global/protocol-kit/",\ + "packageDependencies": [\ + ["@safe-global/protocol-kit", "npm:4.0.4"],\ + ["@ethersproject/abstract-signer", "npm:5.7.0"],\ + ["@ethersproject/bignumber", "npm:5.7.0"],\ + ["@ethersproject/bytes", "npm:5.7.0"],\ + ["@noble/hashes", "npm:1.4.0"],\ + ["@safe-global/safe-core-sdk-types", "npm:5.0.3"],\ + ["@safe-global/safe-deployments", "npm:1.37.3"],\ + ["@types/bn.js", "npm:5.1.5"],\ + ["abitype", "virtual:50c0bb616918b71f9b7ad70bc29d6254b0be2a953e234cac59060d581c642cd881294af5c8ec28e7221aa8e14bc421b0626c2976b086cfd2795d2503310b14da#npm:1.0.5"],\ ["bn.js", "npm:5.2.1"],\ ["ethereumjs-util", "npm:7.1.5"],\ ["ethers", "npm:6.13.1"],\ @@ -2262,7 +2293,15 @@ const RAW_RUNTIME_STATE = "packageLocation": "./.yarn/cache/@safe-global-safe-core-sdk-types-npm-5.0.2-e1ac12735a-53f0221e1c.zip/node_modules/@safe-global/safe-core-sdk-types/",\ "packageDependencies": [\ ["@safe-global/safe-core-sdk-types", "npm:5.0.2"],\ - ["abitype", "virtual:e1ac12735a1a6142b16b0c18f4d069abadf688d06e8edb9c4971f939fb65c057d55015e07cf7a945ed65665e1980beae2649ffe7b99d5d39226d5b65db5bc1b2#npm:1.0.5"]\ + ["abitype", "virtual:50c0bb616918b71f9b7ad70bc29d6254b0be2a953e234cac59060d581c642cd881294af5c8ec28e7221aa8e14bc421b0626c2976b086cfd2795d2503310b14da#npm:1.0.5"]\ + ],\ + "linkType": "HARD"\ + }],\ + ["npm:5.0.3", {\ + "packageLocation": "./.yarn/cache/@safe-global-safe-core-sdk-types-npm-5.0.3-50c0bb6169-e0646c319a.zip/node_modules/@safe-global/safe-core-sdk-types/",\ + "packageDependencies": [\ + ["@safe-global/safe-core-sdk-types", "npm:5.0.3"],\ + ["abitype", "virtual:50c0bb616918b71f9b7ad70bc29d6254b0be2a953e234cac59060d581c642cd881294af5c8ec28e7221aa8e14bc421b0626c2976b086cfd2795d2503310b14da#npm:1.0.5"]\ ],\ "linkType": "HARD"\ }]\ @@ -2275,6 +2314,14 @@ const RAW_RUNTIME_STATE = ["semver", "npm:7.6.2"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.37.3", {\ + "packageLocation": "./.yarn/cache/@safe-global-safe-deployments-npm-1.37.3-ecc590bd3e-3d1fcaac85.zip/node_modules/@safe-global/safe-deployments/",\ + "packageDependencies": [\ + ["@safe-global/safe-deployments", "npm:1.37.3"],\ + ["semver", "npm:7.6.2"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@safe-global/safe-gateway-typescript-sdk", [\ @@ -2285,10 +2332,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ - ["npm:3.21.10", {\ - "packageLocation": "./.yarn/cache/@safe-global-safe-gateway-typescript-sdk-npm-3.21.10-f9fd01348a-602963f3bb.zip/node_modules/@safe-global/safe-gateway-typescript-sdk/",\ + ["npm:3.22.2", {\ + "packageLocation": "./.yarn/cache/@safe-global-safe-gateway-typescript-sdk-npm-3.22.2-cd930c5fbf-7f2b3cab4a.zip/node_modules/@safe-global/safe-gateway-typescript-sdk/",\ "packageDependencies": [\ - ["@safe-global/safe-gateway-typescript-sdk", "npm:3.21.10"]\ + ["@safe-global/safe-gateway-typescript-sdk", "npm:3.22.2"]\ ],\ "linkType": "HARD"\ }]\ @@ -3566,10 +3613,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ - ["virtual:e1ac12735a1a6142b16b0c18f4d069abadf688d06e8edb9c4971f939fb65c057d55015e07cf7a945ed65665e1980beae2649ffe7b99d5d39226d5b65db5bc1b2#npm:1.0.5", {\ - "packageLocation": "./.yarn/__virtual__/abitype-virtual-6c2d7df432/0/cache/abitype-npm-1.0.5-39432a282a-1acd0d9687.zip/node_modules/abitype/",\ + ["virtual:50c0bb616918b71f9b7ad70bc29d6254b0be2a953e234cac59060d581c642cd881294af5c8ec28e7221aa8e14bc421b0626c2976b086cfd2795d2503310b14da#npm:1.0.5", {\ + "packageLocation": "./.yarn/__virtual__/abitype-virtual-fd7a3c3552/0/cache/abitype-npm-1.0.5-39432a282a-1acd0d9687.zip/node_modules/abitype/",\ "packageDependencies": [\ - ["abitype", "virtual:e1ac12735a1a6142b16b0c18f4d069abadf688d06e8edb9c4971f939fb65c057d55015e07cf7a945ed65665e1980beae2649ffe7b99d5d39226d5b65db5bc1b2#npm:1.0.5"],\ + ["abitype", "virtual:50c0bb616918b71f9b7ad70bc29d6254b0be2a953e234cac59060d581c642cd881294af5c8ec28e7221aa8e14bc421b0626c2976b086cfd2795d2503310b14da#npm:1.0.5"],\ ["@types/typescript", null],\ ["@types/zod", null],\ ["typescript", null],\ @@ -11633,7 +11680,7 @@ const RAW_RUNTIME_STATE = ["@scure/bip32", "npm:1.4.0"],\ ["@scure/bip39", "npm:1.3.0"],\ ["@types/typescript", null],\ - ["abitype", "virtual:e1ac12735a1a6142b16b0c18f4d069abadf688d06e8edb9c4971f939fb65c057d55015e07cf7a945ed65665e1980beae2649ffe7b99d5d39226d5b65db5bc1b2#npm:1.0.5"],\ + ["abitype", "virtual:50c0bb616918b71f9b7ad70bc29d6254b0be2a953e234cac59060d581c642cd881294af5c8ec28e7221aa8e14bc421b0626c2976b086cfd2795d2503310b14da#npm:1.0.5"],\ ["isows", "virtual:cc0d0158cc0b97a21158c5fc1575782b4742a1f2abe990dbee708d53dcd39b6be8ba50d533095079ec278b06e4ad47fb33aead80adcfb9282338640bfd92086a#npm:1.0.4"],\ ["typescript", null],\ ["ws", "virtual:62b44a89afe3cf71f91d9a15c255905a0c879fa652d6a4d04977b9668d35332e3616a0f5da5dd26eb85d97f16c77eb71e5fb44ecae23967192e31ea4ad11af7c#npm:8.17.1"]\ @@ -11654,7 +11701,7 @@ const RAW_RUNTIME_STATE = ["@scure/bip32", "npm:1.4.0"],\ ["@scure/bip39", "npm:1.3.0"],\ ["@types/typescript", null],\ - ["abitype", "virtual:e1ac12735a1a6142b16b0c18f4d069abadf688d06e8edb9c4971f939fb65c057d55015e07cf7a945ed65665e1980beae2649ffe7b99d5d39226d5b65db5bc1b2#npm:1.0.5"],\ + ["abitype", "virtual:50c0bb616918b71f9b7ad70bc29d6254b0be2a953e234cac59060d581c642cd881294af5c8ec28e7221aa8e14bc421b0626c2976b086cfd2795d2503310b14da#npm:1.0.5"],\ ["isows", "virtual:cc0d0158cc0b97a21158c5fc1575782b4742a1f2abe990dbee708d53dcd39b6be8ba50d533095079ec278b06e4ad47fb33aead80adcfb9282338640bfd92086a#npm:1.0.4"],\ ["typescript", null],\ ["ws", "virtual:62b44a89afe3cf71f91d9a15c255905a0c879fa652d6a4d04977b9668d35332e3616a0f5da5dd26eb85d97f16c77eb71e5fb44ecae23967192e31ea4ad11af7c#npm:8.17.1"]\ @@ -12490,11 +12537,11 @@ const RAW_RUNTIME_STATE = ["@eslint/js", "npm:9.7.0"],\ ["@gnosis.pm/zodiac", "npm:4.0.3"],\ ["@noble/hashes", "npm:1.4.0"],\ - ["@safe-global/api-kit", "npm:2.4.2"],\ - ["@safe-global/protocol-kit", "npm:4.0.2"],\ + ["@safe-global/api-kit", "npm:2.4.4"],\ + ["@safe-global/protocol-kit", "npm:4.0.4"],\ ["@safe-global/safe-apps-sdk", "npm:9.1.0"],\ - ["@safe-global/safe-core-sdk-types", "npm:5.0.2"],\ - ["@safe-global/safe-gateway-typescript-sdk", "npm:3.21.10"],\ + ["@safe-global/safe-core-sdk-types", "npm:5.0.3"],\ + ["@safe-global/safe-gateway-typescript-sdk", "npm:3.22.2"],\ ["@shazow/whatsabi", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:0.13.2"],\ ["@testing-library/dom", "npm:10.3.1"],\ ["@testing-library/jest-dom", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:6.4.6"],\ diff --git a/extension/.yarn/cache/@safe-global-api-kit-npm-2.4.4-246439c931-578d1632bf.zip b/extension/.yarn/cache/@safe-global-api-kit-npm-2.4.4-246439c931-578d1632bf.zip new file mode 100644 index 00000000..d2fc1d61 Binary files /dev/null and b/extension/.yarn/cache/@safe-global-api-kit-npm-2.4.4-246439c931-578d1632bf.zip differ diff --git a/extension/.yarn/cache/@safe-global-protocol-kit-npm-4.0.4-35473a7fe1-4acdfa6d2c.zip b/extension/.yarn/cache/@safe-global-protocol-kit-npm-4.0.4-35473a7fe1-4acdfa6d2c.zip new file mode 100644 index 00000000..b26e9885 Binary files /dev/null and b/extension/.yarn/cache/@safe-global-protocol-kit-npm-4.0.4-35473a7fe1-4acdfa6d2c.zip differ diff --git a/extension/.yarn/cache/@safe-global-safe-core-sdk-types-npm-5.0.3-50c0bb6169-e0646c319a.zip b/extension/.yarn/cache/@safe-global-safe-core-sdk-types-npm-5.0.3-50c0bb6169-e0646c319a.zip new file mode 100644 index 00000000..15041a40 Binary files /dev/null and b/extension/.yarn/cache/@safe-global-safe-core-sdk-types-npm-5.0.3-50c0bb6169-e0646c319a.zip differ diff --git a/extension/.yarn/cache/@safe-global-safe-deployments-npm-1.37.3-ecc590bd3e-3d1fcaac85.zip b/extension/.yarn/cache/@safe-global-safe-deployments-npm-1.37.3-ecc590bd3e-3d1fcaac85.zip new file mode 100644 index 00000000..91e30d64 Binary files /dev/null and b/extension/.yarn/cache/@safe-global-safe-deployments-npm-1.37.3-ecc590bd3e-3d1fcaac85.zip differ diff --git a/extension/.yarn/cache/@safe-global-safe-gateway-typescript-sdk-npm-3.21.10-f9fd01348a-602963f3bb.zip b/extension/.yarn/cache/@safe-global-safe-gateway-typescript-sdk-npm-3.21.10-f9fd01348a-602963f3bb.zip deleted file mode 100644 index 48b106ed..00000000 Binary files a/extension/.yarn/cache/@safe-global-safe-gateway-typescript-sdk-npm-3.21.10-f9fd01348a-602963f3bb.zip and /dev/null differ diff --git a/extension/.yarn/cache/@safe-global-safe-gateway-typescript-sdk-npm-3.22.2-cd930c5fbf-7f2b3cab4a.zip b/extension/.yarn/cache/@safe-global-safe-gateway-typescript-sdk-npm-3.22.2-cd930c5fbf-7f2b3cab4a.zip new file mode 100644 index 00000000..43c9afd2 Binary files /dev/null and b/extension/.yarn/cache/@safe-global-safe-gateway-typescript-sdk-npm-3.22.2-cd930c5fbf-7f2b3cab4a.zip differ diff --git a/extension/package.json b/extension/package.json index 40af64ff..c82270cd 100644 --- a/extension/package.json +++ b/extension/package.json @@ -27,11 +27,11 @@ "@eslint/js": "^9.7.0", "@gnosis.pm/zodiac": "^4.0.3", "@noble/hashes": "^1.4.0", - "@safe-global/api-kit": "^2.4.2", - "@safe-global/protocol-kit": "^4.0.2", + "@safe-global/api-kit": "^2.4.4", + "@safe-global/protocol-kit": "^4.0.4", "@safe-global/safe-apps-sdk": "^9.1.0", - "@safe-global/safe-core-sdk-types": "^5.0.2", - "@safe-global/safe-gateway-typescript-sdk": "^3.21.10", + "@safe-global/safe-core-sdk-types": "^5.0.3", + "@safe-global/safe-gateway-typescript-sdk": "^3.22.2", "@shazow/whatsabi": "^0.13.2", "@testing-library/dom": "^10.3.1", "@testing-library/jest-dom": "^6.4.6", diff --git a/extension/src/app.tsx b/extension/src/app.tsx index 77dd3fb2..bee82795 100644 --- a/extension/src/app.tsx +++ b/extension/src/app.tsx @@ -12,7 +12,7 @@ import ProvideProvider from './browser/ProvideProvider' import { ProvideState } from './state' import ZodiacToastContainer from './components/Toast' import { pushLocation } from './location' -import { ProvideMetaMask, ProvideTenderly } from './providers' +import { ProvideMetaMask } from './providers' import { useMatchConnectionsRoute, usePushConnectionsRoute } from './routing' import { ProvideRoutes } from './routes' import { useRoutes, useUpdateLastUsedRoute } from './routes/routeHooks' @@ -55,12 +55,10 @@ root.render( - - - - - - + + + + diff --git a/extension/src/background.ts b/extension/src/background.ts index d6ca2977..46b4b825 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -13,6 +13,7 @@ const activeExtensionTabs = new Set() const startTrackingTab = (tabId: number) => { activeExtensionTabs.add(tabId) + clearStaleRules() updateHeadersRule() console.log('Pilot: started tracking tab', tabId) } @@ -21,6 +22,7 @@ const stopTrackingTab = (tabId: number) => { removeRpcRedirectRules(tabId) activeExtensionTabs.delete(tabId) simulatingExtensionTabs.delete(tabId) + clearStaleRules() updateHeadersRule() console.log('Pilot: stopped tracking tab', tabId) } @@ -99,12 +101,10 @@ chrome.action.onClicked.addListener(toggle) // a fork network. const simulatingExtensionTabs = new Map() -// Hash the RPC URL+ tab ID to a number, so we can use it as a declarativeNetRequest rule ID. +// Hash the RPC URL + tab ID to a number, so we can use it as a declarativeNetRequest rule ID. // Implementation taken from https://github.com/darkskyapp/string-hash (CC0 Public Domain) function hash(rpcUrl: string, tabId: number) { - const urlComponents = rpcUrl.split('/') - const forkId = urlComponents[urlComponents.length - 1] - const str = `${tabId}:${forkId}` + const str = `${tabId}:${rpcUrl}` const MAX_RULE_ID = 0xffffff // chrome throws an error if the rule ID is too large ("expected integer, got number") let hash = 5381, @@ -120,6 +120,46 @@ function hash(rpcUrl: string, tabId: number) { return (hash >>> 0) % MAX_RULE_ID } +async function clearStaleRules() { + const openTabIds = await new Promise>((resolve) => { + chrome.tabs.query({}, (tabs) => { + resolve(new Set(tabs.map((tab) => tab.id).filter(Boolean) as number[])) + }) + }) + + // clear activeExtensionTabs that are not open anymore + activeExtensionTabs.difference(openTabIds).forEach((tabId) => { + activeExtensionTabs.delete(tabId) + }) + console.log({ activeExtensionTabs, openTabIds }) + + // clear simulatingExtensionTabs that are not active (= extension is activated) anymore + const simulatingTabIds = new Set(simulatingExtensionTabs.keys()) + simulatingTabIds.difference(activeExtensionTabs).forEach((tabId) => { + simulatingExtensionTabs.delete(tabId) + }) + + // remove rules for tabs that are not simulating anymore + const staleRules = await new Promise( + (resolve) => { + chrome.declarativeNetRequest.getSessionRules((rules) => { + resolve( + rules.filter( + (rule) => + rule.condition.tabIds?.length === 1 && + !simulatingExtensionTabs.has(rule.condition.tabIds[0]) + ) + ) + }) + } + ) + chrome.declarativeNetRequest.updateSessionRules({ + removeRuleIds: staleRules.map((r) => r.id), + }) + + console.debug('Cleared stale rules', staleRules) +} + const updateRpcRedirectRules = (tabId: number) => { const fork = simulatingExtensionTabs.get(tabId) if (!fork) { @@ -156,6 +196,10 @@ const updateRpcRedirectRules = (tabId: number) => { addRules, removeRuleIds: ruleIds, }) + + chrome.declarativeNetRequest.getSessionRules((rules) => { + console.debug('RPC redirect rules updated', tabId, rules) + }) } const removeRpcRedirectRules = (tabId: number) => { @@ -176,9 +220,6 @@ const removeRpcRedirectRules = (tabId: number) => { 735219801 ) ) - chrome.declarativeNetRequest.getSessionRules((rules) => - console.log('removeRpcRedirectRules getSessionRules', rules) - ) } chrome.runtime.onMessage.addListener((message, sender) => { @@ -202,6 +243,7 @@ chrome.runtime.onMessage.addListener((message, sender) => { rpcUrl ) } + if (message.type === 'stopSimulating') { console.log('stopSimulating', sender.tab.id, { simulatingExtensionTabs }) simulatingExtensionTabs.delete(sender.tab.id) @@ -233,6 +275,9 @@ chrome.webRequest.onBeforeRequest.addListener( // don't consider requests that are already redirected to the fork RPC if (details.url === simulatingExtensionTabs.get(details.tabId)?.rpcUrl) return + // ignore requests to fork RPCs + if (details.url.startsWith('https://virtual.mainnet.rpc.tenderly.co/')) + return // only consider requests with a JSON RPC body if (!getJsonRpcBody(details)) return diff --git a/extension/src/bridge/SafeAppBridge.ts b/extension/src/bridge/SafeAppBridge.ts index 0716b8cc..02f599ed 100644 --- a/extension/src/bridge/SafeAppBridge.ts +++ b/extension/src/bridge/SafeAppBridge.ts @@ -235,7 +235,7 @@ export default class SafeAppBridge { // assume on-chain signature const safeTxHash = await this.provider.request({ method: 'eth_signTypedData_v4', - params: [typedData], + params: [this.connection.avatarAddress, JSON.stringify(typedData)], }) return { safeTxHash } }, diff --git a/extension/src/browser/Drawer/DecodedTransaction.tsx b/extension/src/browser/Drawer/DecodedTransaction.tsx index 6ca5e0b5..ef44451e 100644 --- a/extension/src/browser/Drawer/DecodedTransaction.tsx +++ b/extension/src/browser/Drawer/DecodedTransaction.tsx @@ -8,25 +8,23 @@ interface Props { functionFragment: FunctionFragment data: Result } -const DecodedTransaction: React.FC = ({ functionFragment, data }) => { - return ( -
- {functionFragment.inputs.length > 0 && ( -
- {functionFragment.inputs.map((input, i) => ( - - ))} -
- )} -
- ) -} +const DecodedTransaction: React.FC = ({ functionFragment, data }) => ( +
+ {functionFragment.inputs.length > 0 && ( +
+ {functionFragment.inputs.map((input, i) => ( + + ))} +
+ )} +
+) export default DecodedTransaction diff --git a/extension/src/browser/Drawer/RolePermissionCheck.tsx b/extension/src/browser/Drawer/RolePermissionCheck.tsx index 58abc991..6897988f 100644 --- a/extension/src/browser/Drawer/RolePermissionCheck.tsx +++ b/extension/src/browser/Drawer/RolePermissionCheck.tsx @@ -4,7 +4,7 @@ import { RiGroupLine } from 'react-icons/ri' import { Flex, Tag } from '../../components' import { useApplicableTranslation } from '../../transactionTranslations' -import { JsonRpcError, Route } from '../../types' +import { Eip1193Provider, JsonRpcError, Route } from '../../types' import { decodeRolesV1Error } from '../../utils' import { decodeGenericError, decodeRolesV2Error } from '../../utils/decodeError' @@ -12,8 +12,6 @@ import CopyToClipboard from './CopyToClipboard' import { Translate } from './Translate' import classes from './style.module.css' import { useRoute } from '../../routes' -import { useTenderlyProvider } from '../../providers' -import { TenderlyProvider } from '../../providers/ProvideTenderly' import { TransactionState } from '../../state' import { MetaTransactionData } from '@safe-global/safe-core-sdk-types' import { toQuantity, ZeroAddress } from 'ethers' @@ -23,11 +21,12 @@ import { planExecution, Route as SerRoute, } from 'ser-kit' +import { useProvider } from '../ProvideProvider' const simulateRolesTransaction = async ( encodedTransaction: MetaTransactionData, route: Route, - tenderlyProvider: TenderlyProvider + provider: Eip1193Provider ) => { const routeWithInitiator = ( route.initiator ? route : { ...route, initiator: ZeroAddress } @@ -50,7 +49,7 @@ const simulateRolesTransaction = async ( } try { - await tenderlyProvider.request({ + await provider.request({ method: 'eth_estimateGas', params: [tx], }) @@ -86,7 +85,7 @@ const RolePermissionCheck: React.FC<{ }> = ({ transactionState, index, mini = false }) => { const [error, setError] = useState(undefined) const { route } = useRoute() - const tenderlyProvider = useTenderlyProvider() + const provider = useProvider() const translationAvailable = !!useApplicableTranslation( transactionState.transaction @@ -94,11 +93,12 @@ const RolePermissionCheck: React.FC<{ useEffect(() => { let canceled = false + if (!provider) return simulateRolesTransaction( transactionState.transaction, route, - tenderlyProvider + provider ).then((error) => { if (!canceled) setError(error) }) @@ -106,7 +106,7 @@ const RolePermissionCheck: React.FC<{ return () => { canceled = true } - }, [transactionState, route, tenderlyProvider]) + }, [transactionState, route, provider]) if (error === undefined) return null diff --git a/extension/src/browser/Drawer/SimulationStatus.tsx b/extension/src/browser/Drawer/SimulationStatus.tsx index f7d39609..c085ccd9 100644 --- a/extension/src/browser/Drawer/SimulationStatus.tsx +++ b/extension/src/browser/Drawer/SimulationStatus.tsx @@ -2,17 +2,18 @@ import React from 'react' import { RiExternalLinkLine, RiGitBranchLine } from 'react-icons/ri' import { Flex, Spinner, Tag } from '../../components' -import { useTenderlyProvider } from '../../providers' import classes from './style.module.css' import { TransactionState } from '../../state' import { ExecutionStatus } from '../../state/reducer' +import { useProvider } from '../ProvideProvider' const SimulationStatus: React.FC<{ transactionState: TransactionState mini?: boolean }> = ({ transactionState, mini = false }) => { - const tenderlyProvider = useTenderlyProvider() + const provider = useProvider() + if (mini) { return ( <> @@ -22,9 +23,9 @@ const SimulationStatus: React.FC<{ {transactionState.status === ExecutionStatus.SUCCESS && ( } color="success"> )} - {transactionState.status === ExecutionStatus.REVERTED || + {transactionState.status === ExecutionStatus.FAILED || (transactionState.status === - ExecutionStatus.MODULE_TRANSACTION_REVERTED && ( + ExecutionStatus.META_TRANSACTION_REVERTED && ( } color="danger"> ))} @@ -56,15 +57,15 @@ const SimulationStatus: React.FC<{ Success )} - {transactionState.status === ExecutionStatus.REVERTED && ( + {transactionState.status === ExecutionStatus.FAILED && ( } color="danger"> - Reverted + Failed )} {transactionState.status === - ExecutionStatus.MODULE_TRANSACTION_REVERTED && ( + ExecutionStatus.META_TRANSACTION_REVERTED && ( } color="danger"> - Module transaction reverted + Reverted )} @@ -72,7 +73,7 @@ const SimulationStatus: React.FC<{ {transactionState.transactionHash && ( { throw new Error('This is only supported when using ForkProvider') } - await provider.refork() + await provider.deleteFork() // re-simulate all new transactions (assuming the already submitted ones have already been mined on the fresh fork) for (const transaction of newTransactions) { diff --git a/extension/src/browser/ProvideProvider.tsx b/extension/src/browser/ProvideProvider.tsx index 68a7f3c0..74d81679 100644 --- a/extension/src/browser/ProvideProvider.tsx +++ b/extension/src/browser/ProvideProvider.tsx @@ -3,16 +3,17 @@ import React, { ReactNode, useCallback, useContext, - useMemo, + useEffect, + useRef, } from 'react' -import { ForkProvider, useTenderlyProvider } from '../providers' +import { ForkProvider } from '../providers' import { useRoute } from '../routes' import { Eip1193Provider } from '../types' import { useDispatch, useNewTransactions } from '../state' import { fetchContractInfo } from '../utils/abi' import { ExecutionStatus } from '../state/reducer' -import { AbiCoder, BrowserProvider, TransactionReceipt } from 'ethers' +import { AbiCoder, BrowserProvider, id, TransactionReceipt } from 'ethers' import { ConnectionType, execute, @@ -22,12 +23,16 @@ import { planExecution, Route as SerRoute, } from 'ser-kit' +import { useBeforeUnload } from '../utils' +import { MetaTransactionData } from '@safe-global/safe-core-sdk-types' interface Props { children: ReactNode } -const ProviderContext = createContext(null) +const ProviderContext = createContext< + (Eip1193Provider & { getTransactionLink(txHash: string): string }) | null +>(null) export const useProvider = () => useContext(ProviderContext) const SubmitTransactionsContext = createContext<(() => Promise) | null>( @@ -37,7 +42,6 @@ export const useSubmitTransactions = () => useContext(SubmitTransactionsContext) const ProvideProvider: React.FC = ({ children }) => { const { provider, route, chainId } = useRoute() - const tenderlyProvider = useTenderlyProvider() const dispatch = useDispatch() const transactions = useNewTransactions() @@ -53,99 +57,116 @@ const ProvideProvider: React.FC = ({ children }) => { parsePrefixedAddress(avatarWaypoint.connection.from)) || [] - const forkProvider = useMemo( - () => - tenderlyProvider && - new ForkProvider(tenderlyProvider, { - avatarAddress, - moduleAddress: - connectionType === ConnectionType.IS_ENABLED - ? connectedFrom - : undefined, - ownerAddress: - connectionType === ConnectionType.OWNS ? connectedFrom : undefined, - - async onBeforeTransactionSend(snapshotId, transaction) { - // Immediately update the state with the transaction so that the UI can show it as pending. - dispatch({ - type: 'APPEND_TRANSACTION', - payload: { transaction, snapshotId }, - }) - - // 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}`, - chainId - ) - dispatch({ - type: 'DECODE_TRANSACTION', - payload: { - snapshotId, - contractInfo, - }, - }) + const moduleAddress = + connectionType === ConnectionType.IS_ENABLED ? connectedFrom : undefined + const ownerAddress = + connectionType === ConnectionType.OWNS ? connectedFrom : undefined + + const onBeforeTransactionSend = useCallback( + async (snapshotId: string, transaction: MetaTransactionData) => { + // Immediately update the state with the transaction so that the UI can show it as pending. + dispatch({ + type: 'APPEND_TRANSACTION', + payload: { transaction, snapshotId }, + }) + + // 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}`, + chainId + ) + dispatch({ + type: 'DECODE_TRANSACTION', + payload: { + snapshotId, + contractInfo, }, + }) + }, + [chainId, dispatch] + ) - async onTransactionSent(snapshotId, transactionHash) { - dispatch({ - type: 'CONFIRM_TRANSACTION', - payload: { - snapshotId, - transactionHash, - }, - }) - - const receipt = await new BrowserProvider( - tenderlyProvider - ).getTransactionReceipt(transactionHash) - if (!receipt?.status) { - dispatch({ - type: 'UPDATE_TRANSACTION_STATUS', - payload: { - snapshotId, - status: ExecutionStatus.REVERTED, - }, - }) - return - } - - if ( - receipt.logs.length === 1 && - connectionType === ConnectionType.IS_ENABLED && - isExecutionFromModuleFailure( - receipt.logs[0], - avatarAddress, - connectedFrom - ) - ) { - dispatch({ - type: 'UPDATE_TRANSACTION_STATUS', - payload: { - snapshotId, - status: ExecutionStatus.MODULE_TRANSACTION_REVERTED, - }, - }) - } else { - dispatch({ - type: 'UPDATE_TRANSACTION_STATUS', - payload: { - snapshotId, - status: ExecutionStatus.SUCCESS, - }, - }) - } + const onTransactionSent = useCallback( + async ( + snapshotId: string, + transactionHash: string, + provider: Eip1193Provider + ) => { + dispatch({ + type: 'CONFIRM_TRANSACTION', + payload: { + snapshotId, + transactionHash, }, - }), - [ - tenderlyProvider, - avatarAddress, - connectionType, - connectedFrom, - chainId, - dispatch, - ] + }) + + const receipt = await new BrowserProvider(provider).getTransactionReceipt( + transactionHash + ) + if (!receipt?.status) { + dispatch({ + type: 'UPDATE_TRANSACTION_STATUS', + payload: { + snapshotId, + status: ExecutionStatus.FAILED, + }, + }) + return + } + + if ( + receipt.logs.length === 1 && + isExecutionFailure(receipt.logs[0], avatarAddress, moduleAddress) + ) { + dispatch({ + type: 'UPDATE_TRANSACTION_STATUS', + payload: { + snapshotId, + status: ExecutionStatus.META_TRANSACTION_REVERTED, + }, + }) + } else { + dispatch({ + type: 'UPDATE_TRANSACTION_STATUS', + payload: { + snapshotId, + status: ExecutionStatus.SUCCESS, + }, + }) + } + }, + [dispatch, avatarAddress, moduleAddress] ) + const forkProviderRef = useRef(null) + + // whenever anything changes in the connection settings, we delete the current fork and start afresh + useEffect(() => { + forkProviderRef.current = new ForkProvider({ + chainId, + avatarAddress, + moduleAddress, + ownerAddress, + onBeforeTransactionSend, + onTransactionSent, + }) + return () => { + forkProviderRef.current?.deleteFork() + } + }, [ + chainId, + avatarAddress, + moduleAddress, + ownerAddress, + onBeforeTransactionSend, + onTransactionSent, + ]) + + // delete fork when closing browser tab (the effect teardown won't be executed in that case) + useBeforeUnload(() => { + forkProviderRef.current?.deleteFork() + }) + const submitTransactions = useCallback(async () => { const metaTransactions = transactions.map((txState) => txState.transaction) @@ -188,8 +209,12 @@ const ProvideProvider: React.FC = ({ children }) => { return batchTransactionHash }, [transactions, provider, dispatch, route]) + if (!forkProviderRef.current) { + return null + } + return ( - + {children} @@ -199,17 +224,20 @@ const ProvideProvider: React.FC = ({ children }) => { export default ProvideProvider -const isExecutionFromModuleFailure = ( +const isExecutionFailure = ( log: TransactionReceipt['logs'][0], avatarAddress: string, moduleAddress?: string ) => { - return ( - log.address.toLowerCase() === avatarAddress.toLowerCase() && - log.topics[0] === - '0xacd2c8702804128fdb0db2bb49f6d127dd0181c13fd45dbfe16de0930e2bd375' && // ExecutionFromModuleFailure(address) - (!moduleAddress || + if (log.address.toLowerCase() !== avatarAddress.toLowerCase()) return false + + if (moduleAddress) { + return ( + log.topics[0] === id('ExecutionFromModuleFailure(address)') && log.topics[1] === - AbiCoder.defaultAbiCoder().encode(['address'], [moduleAddress])) - ) + AbiCoder.defaultAbiCoder().encode(['address'], [moduleAddress]) + ) + } else { + return log.topics[0] === id('ExecutionFailure(bytes32, uint256)') + } } diff --git a/extension/src/integrations/safe/interface.ts b/extension/src/integrations/safe/interface.ts index 2e2a11bc..927d2dad 100644 --- a/extension/src/integrations/safe/interface.ts +++ b/extension/src/integrations/safe/interface.ts @@ -1,8 +1,5 @@ import { Interface } from 'ethers' export const safeInterface = new Interface([ - 'function execTransaction(address to, uint256 value, bytes data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, bytes signatures) returns (bool success)', - 'function changeThreshold(uint256 _threshold)', - 'function addOwnerWithThreshold(address owner, uint256 _threshold)', 'function getMessageHashForSafe(address safe, bytes message) view returns (bytes32)', ]) diff --git a/extension/src/integrations/safe/signing.ts b/extension/src/integrations/safe/signing.ts index 39aba047..eda1980e 100644 --- a/extension/src/integrations/safe/signing.ts +++ b/extension/src/integrations/safe/signing.ts @@ -1,6 +1,11 @@ import { MetaTransactionData } from '@safe-global/safe-core-sdk-types' import { EIP712TypedData } from '@safe-global/safe-gateway-typescript-sdk' -import { Contract, hashMessage, toUtf8String, TypedDataEncoder } from 'ethers' +import { + Contract, + hashMessage as ethersHashMessage, + toUtf8String, + TypedDataEncoder, +} from 'ethers' const SIGN_MESSAGE_LIB_ADDRESS = '0xd53cd0aB83D845Ac265BE939c57F53AD838012c9' const SIGN_MESSAGE_LIB_ABI = [ @@ -16,7 +21,7 @@ const signMessageLib = new Contract( export const signMessage = (message: string): MetaTransactionData => ({ to: SIGN_MESSAGE_LIB_ADDRESS, data: signMessageLib.interface.encodeFunctionData('signMessage', [ - hashMessage(decode(message)), + hashMessage(message), ]), value: '0', operation: 1, @@ -40,6 +45,9 @@ export const signTypedData = (data: EIP712TypedData) => { } } +export const hashMessage = (message: string) => + ethersHashMessage(decode(message)) + const decode = (message: string): string => { if (!message.startsWith('0x')) { return message diff --git a/extension/src/providers/ForkProvider.ts b/extension/src/providers/ForkProvider.ts index 5d67021e..9f5199fe 100644 --- a/extension/src/providers/ForkProvider.ts +++ b/extension/src/providers/ForkProvider.ts @@ -1,18 +1,21 @@ import EventEmitter from 'events' import { ContractFactories, KnownContracts } from '@gnosis.pm/zodiac' -import { BrowserProvider, toQuantity } from 'ethers' -import { - MetaTransactionData, - TransactionOptions, -} from '@safe-global/safe-core-sdk-types' -import { generatePreValidatedSignature } from '@safe-global/protocol-kit/dist/src/utils' +import { BrowserProvider, toQuantity, ZeroAddress } from 'ethers' +import { MetaTransactionData } from '@safe-global/safe-core-sdk-types' import { Eip1193Provider, TransactionData } from '../types' -import { TenderlyProvider } from './ProvideTenderly' -import { safeInterface } from '../integrations/safe' +import TenderlyProvider from './TenderlyProvider' +import { initSafeProtocolKit, safeInterface } from '../integrations/safe' import { translateSignSnapshotVote } from '../transactionTranslations/signSnapshotVote' -import { typedDataHash } from '../integrations/safe/signing' +import { + hashMessage, + signMessage, + signTypedData, + typedDataHash, +} from '../integrations/safe/signing' +import { ChainId } from 'ser-kit' +import { decodeGenericError } from '../utils' class UnsupportedMethodError extends Error { code = 4200 @@ -23,39 +26,47 @@ interface Handlers { checkpointId: string, metaTx: MetaTransactionData ): void - onTransactionSent(checkpointId: string, hash: string): void + onTransactionSent( + checkpointId: string, + hash: string, + provider: Eip1193Provider + ): void } +/** This is separated from TenderlyProvider to provide an abstraction over Tenderly implementation details. That way we will be able to more easily plug in alternative simulation back-ends. */ class ForkProvider extends EventEmitter { private provider: TenderlyProvider - private handlers: Handlers - private avatarAddress: string + private chainId: ChainId + private avatarAddress: string private moduleAddress: string | undefined private ownerAddress: string | undefined + private handlers: Handlers + private blockGasLimitPromise: Promise private pendingMetaTransaction: Promise | undefined - - constructor( - provider: TenderlyProvider, - { - avatarAddress, - moduleAddress, - ownerAddress, - - ...handlers - }: { - avatarAddress: string - /** If set, will simulate the transaction though an `execTransactionFromModule` call */ - moduleAddress?: string - /** If set, will simulate the transaction though an `execTransaction` call */ - ownerAddress?: string - } & Handlers - ) { + private isInitialized = false + + constructor({ + chainId, + avatarAddress, + moduleAddress, + ownerAddress, + + ...handlers + }: { + chainId: ChainId + avatarAddress: string + /** If set, will simulate transactions using respective `execTransactionFromModule` calls */ + moduleAddress?: string + /** If set, will enable the the ownerAddress as a module and simulate using `execTransactionFromModule` calls. If neither `moduleAddress` nor `ownerAddress` is set, it will enable a dummy module 0xfacade */ + ownerAddress?: string + } & Handlers) { super() - this.provider = provider + this.chainId = chainId + this.provider = new TenderlyProvider(chainId) this.avatarAddress = avatarAddress this.moduleAddress = moduleAddress this.ownerAddress = ownerAddress @@ -91,38 +102,63 @@ class ForkProvider extends EventEmitter { } case 'eth_sign': { - // TODO support this via Safe's SignMessageLib throw new UnsupportedMethodError('eth_sign is not supported') } + case 'personal_sign': { + const [message, from] = params + if (from.toLowerCase() !== this.avatarAddress.toLowerCase()) { + throw new Error('personal_sign only supported for the avatar address') + } + const signTx = signMessage(message) + const safeTxHash = await this.sendMetaTransaction(signTx) + + console.log('message signed', { + safeTxHash, + messageHash: hashMessage(message), + }) + + return '0x' + } + case 'eth_signTypedData': case 'eth_signTypedData_v4': { - console.log('eth_signTypedData_v4', params) + const [from, dataString] = params + if (from.toLowerCase() !== this.avatarAddress.toLowerCase()) { + throw new Error( + 'eth_signTypedData_v4 only supported for the avatar address' + ) + } + const data = JSON.parse(dataString) + + const dataHash = typedDataHash(data) + const safeMessageHash = await safeInterface.encodeFunctionData( + 'getMessageHashForSafe', + [this.avatarAddress, dataHash] + ) // special handling for Snapshot vote signatures - const tx = translateSignSnapshotVote(params[0] || {}) - if (tx) { - const safeTxHash = await this.sendMetaTransaction(tx) - - // TODO we don't even need this, but for now we keep it for debugging purposes - const safeMessageHash = await safeInterface.encodeFunctionData( - 'getMessageHashForSafe', - [this.avatarAddress, typedDataHash(params[0])] - ) - console.log('Snapshot vote signed', { + const snapshotVoteTx = translateSignSnapshotVote(data || {}) + if (snapshotVoteTx) { + const safeTxHash = await this.sendMetaTransaction(snapshotVoteTx) + + console.log('Snapshot vote EIP-712 message signed', { safeTxHash, safeMessageHash, - typedDataHash: typedDataHash(params[0]), + typedDataHash: dataHash, }) + } else { + // default EIP-712 signature handling + const signTx = signTypedData(data) + const safeTxHash = await this.sendMetaTransaction(signTx) - // The Safe App SDK expects a response in the format of `{ safeTxHash }` for on-chain signatures. - // So we make the safeTxHash available by returning it as the signature. - return safeTxHash + console.log('EIP-712 message signed', { + safeTxHash, + safeMessageHash, + typedDataHash: dataHash, + }) } - // TODO support this via Safe's SignMessageLib - throw new UnsupportedMethodError( - 'eth_signTypedData_v4 is not supported' - ) + return '0x' } case 'eth_sendTransaction': { @@ -155,18 +191,23 @@ class ForkProvider extends EventEmitter { const send = this.pendingMetaTransaction ? async () => { await this.pendingMetaTransaction - return await this._sendMetaTransaction(metaTx) + return await this.sendMetaTransactionIsSeries(metaTx) } - : async () => await this._sendMetaTransaction(metaTx) + : async () => await this.sendMetaTransactionIsSeries(metaTx) // Synchronously update `this.pendingMetaTransaction` so subsequent `sendMetaTransaction()` calls will go to the back of the queue this.pendingMetaTransaction = send() return await this.pendingMetaTransaction } - private async _sendMetaTransaction( + private async sendMetaTransactionIsSeries( metaTx: MetaTransactionData ): Promise { + if (!this.isInitialized) { + // we lazily initialize the fork (making the Safe ready for simulating transactions) when the first transaction is sent + await this.initFork() + } + const isDelegateCall = metaTx.operation === 1 if (isDelegateCall && !this.moduleAddress && !this.ownerAddress) { throw new Error('delegatecall requires moduleAddress or ownerAddress') @@ -183,48 +224,63 @@ class ForkProvider extends EventEmitter { this.handlers.onBeforeTransactionSend(checkpointId, metaTx) + let from = this.moduleAddress || this.ownerAddress || DUMMY_MODULE_ADDRESS + if (from === ZeroAddress) from = DUMMY_MODULE_ADDRESS + // correctly route the meta tx through the avatar - let tx: TransactionData & TransactionOptions - if (this.moduleAddress) { - tx = execTransactionFromModule( - metaTx, - this.avatarAddress, - this.moduleAddress, - await this.blockGasLimitPromise - ) - } else if (this.ownerAddress) { - tx = execTransaction( - metaTx, - this.avatarAddress, - this.ownerAddress, - await this.blockGasLimitPromise - ) - } else { - // no module or owner address, simulate with avatar as sender - // note: this is a theoretical case only atm - tx = { - to: metaTx.to, - data: metaTx.data, - value: toQuantity(BigInt(metaTx.value)), - from: this.avatarAddress, - } - } + const tx = execTransactionFromModule( + metaTx, + this.avatarAddress, + from, + await this.blockGasLimitPromise + ) // execute transaction in fork const result = await this.provider.request({ method: 'eth_sendTransaction', params: [tx], }) - this.handlers.onTransactionSent(checkpointId, result) + this.handlers.onTransactionSent(checkpointId, result, this.provider) return result } - async refork(): Promise { - await this.provider.refork() + async initFork(): Promise { + console.log('Initializing fork for simulation...') + + await prepareSafeForSimulation( + { + chainId: this.chainId, + avatarAddress: this.avatarAddress, + moduleAddress: this.moduleAddress, + ownerAddress: this.ownerAddress, + }, + this.provider + ) + + // 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: this.chainId, + rpcUrl: this.provider.publicRpc, + }, + '*' + ) + + this.isInitialized = true } async deleteFork(): Promise { + // notify the background script to stop intercepting JSON RPC requests + window.postMessage({ type: 'stopSimulating', toBackground: true }, '*') await this.provider.deleteFork() + this.isInitialized = false + } + + getTransactionLink(txHash: string) { + return this.provider.getTransactionLink(txHash) } } @@ -260,43 +316,56 @@ const execTransactionFromModule = ( } const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' - -/** Encode an execTransaction call by the given owner (address must be an actual owner of the Safe) */ -// for reference: https://github.com/safe-global/safe-wallet-web/blob/dev/src/components/tx/security/tenderly/utils.ts#L213 -export function execTransaction( - tx: MetaTransactionData & TransactionOptions & { gas?: string | number }, - avatarAddress: string, - ownerAddress: string, - blockGasLimit: bigint -): TransactionData & { gas?: string } { - const signature = generatePreValidatedSignature(ownerAddress) - const data = safeInterface.encodeFunctionData('execTransaction', [ - tx.to, - tx.value, - tx.data, - tx.operation, - tx.gasLimit || tx.gas || 0, - 0, - tx.gasPrice || 0, - ZERO_ADDRESS, - ZERO_ADDRESS, - signature.staticPart() + signature.dynamicPart(), - ]) - - return { - to: avatarAddress, - data, - value: '0x0', - from: ownerAddress, - // We simulate setting the entire block gas limit as the gas limit for the transaction - gas: toQuantity(blockGasLimit / 2n), // 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', // doesn't seem to be required - } -} +const DUMMY_MODULE_ADDRESS = '0xfacade0000000000000000000000000000000000' const readBlockGasLimit = async (provider: Eip1193Provider) => { const browserProvider = new BrowserProvider(provider) const block = await browserProvider.getBlock('latest') return block?.gasLimit || 30_000_000n } + +async function prepareSafeForSimulation( + { + chainId, + avatarAddress, + moduleAddress, + ownerAddress, + }: { + chainId: ChainId + avatarAddress: string + moduleAddress?: string + ownerAddress?: string + }, + provider: TenderlyProvider +) { + const safe = await initSafeProtocolKit(chainId, avatarAddress) + + // If we simulate as a Safe owner, we could either use execTransaction and override the threshold to 1. + // However, enabling the owner as a module seems like a more simple approach. + + let from = moduleAddress || ownerAddress || DUMMY_MODULE_ADDRESS + if (from === ZeroAddress) from = DUMMY_MODULE_ADDRESS + + const iface = safe.getContractManager().safeContract?.contract.interface + if (!iface) { + throw new Error('Safe contract not found') + } + + try { + await provider.request({ + method: 'eth_sendTransaction', + params: [ + { + to: avatarAddress, + data: iface.encodeFunctionData('enableModule', [from]), + from: avatarAddress, + }, + ], + }) + } catch (e) { + // ignore revert indicating that the module is already enabled + if (decodeGenericError(e as any) !== 'GS102') { + throw e + } + } +} diff --git a/extension/src/providers/ProvideTenderly.tsx b/extension/src/providers/TenderlyProvider.tsx similarity index 52% rename from extension/src/providers/ProvideTenderly.tsx rename to extension/src/providers/TenderlyProvider.tsx index 12385dcc..9c05aac4 100644 --- a/extension/src/providers/ProvideTenderly.tsx +++ b/extension/src/providers/TenderlyProvider.tsx @@ -1,138 +1,26 @@ import EventEmitter from 'events' -import React, { useContext, useEffect, useMemo } from 'react' import { customAlphabet } from 'nanoid' -import { useRoute } from '../routes' import { JsonRpcRequest } from '../types' -import { useBeforeUnload } from '../utils' -import { initSafeProtocolKit, safeInterface } from '../integrations/safe' import { getReadOnlyProvider } from './readOnlyProvider' import { ChainId } from 'ser-kit' -import { asLegacyConnection } from '../routes/legacyConnectionMigrations' import { JsonRpcProvider } from 'ethers' const slug = customAlphabet('abcdefghijklmnopqrstuvwxyz0123456789') -const TenderlyContext = React.createContext(null) - -export const useTenderlyProvider = (): TenderlyProvider => { - const value = useContext(TenderlyContext) - if (!value) throw new Error('must be wrapped in ') - return value -} - -const ProvideTenderly: React.FC<{ children: React.ReactNode }> = ({ - children, -}) => { - const { chainId, route } = useRoute() - const { avatarAddress, moduleAddress, pilotAddress } = - asLegacyConnection(route) - - const tenderlyProvider = useMemo(() => { - return new TenderlyProvider(chainId) - }, [chainId]) - - // whenever anything changes in the connection settings, we delete the current fork and start afresh - useEffect(() => { - prepareSafeForSimulation( - { chainId, avatarAddress, moduleAddress, pilotAddress }, - tenderlyProvider - ) - - return () => { - tenderlyProvider.deleteFork() - } - }, [tenderlyProvider, chainId, avatarAddress, moduleAddress, pilotAddress]) - - // delete fork when closing browser tab (the effect teardown won't be executed in that case) - useBeforeUnload(() => { - if (tenderlyProvider) tenderlyProvider.deleteFork() - }) - - if (!tenderlyProvider) return null - - return ( - - {children} - - ) -} - -export default ProvideTenderly - -async function prepareSafeForSimulation( - { - chainId, - avatarAddress, - moduleAddress, - pilotAddress, - }: { - chainId: ChainId - avatarAddress: string - moduleAddress?: string - pilotAddress?: string - }, - tenderlyProvider: TenderlyProvider -) { - const safe = await initSafeProtocolKit(chainId, avatarAddress) - - // If we simulate as a Safe owner, we might have to override the owners & threshold of the Safe to allow single signature transactions - if (!moduleAddress) { - const [owners, threshold] = await Promise.all([ - safe.getOwners(), - safe.getThreshold(), - ]) - - // default to first owner if no pilot address is provided - if (!pilotAddress) pilotAddress = owners[0] - - const pilotIsOwner = owners.some( - (owner) => owner.toLowerCase() === pilotAddress!.toLowerCase() - ) - - if (!pilotIsOwner) { - // the pilot account is not an owner, so we need to make it one and set the threshold to 1 at the same time - await tenderlyProvider.request({ - method: 'eth_sendTransaction', - params: [ - { - to: avatarAddress, - data: safeInterface.encodeFunctionData('addOwnerWithThreshold', [ - pilotAddress, - 1, - ]), - from: avatarAddress, - }, - ], - }) - } else if (threshold > 1) { - // doesn't allow to execute with single signature, so we need to override the threshold - await tenderlyProvider.request({ - method: 'eth_sendTransaction', - params: [ - { - to: avatarAddress, - data: safeInterface.encodeFunctionData('changeThreshold', [1]), - from: avatarAddress, - }, - ], - }) - } - } -} - -export class TenderlyProvider extends EventEmitter { +export default class TenderlyProvider extends EventEmitter { private chainId: number private forkProviderPromise: Promise | undefined private vnetId: string | undefined - private publicRpcSlug: string | undefined private blockNumber: number | undefined private tenderlyVnetApi: string private throttledIncreaseBlock: () => void + publicRpc: string | undefined + constructor(chainId: ChainId) { super() this.chainId = chainId @@ -193,21 +81,12 @@ export class TenderlyProvider extends EventEmitter { return result } - async refork() { - this.deleteFork() - this.forkProviderPromise = this.createFork(this.chainId) - return await this.forkProviderPromise - } - async deleteFork() { - // notify the background script to stop intercepting JSON RPC requests - window.postMessage({ type: 'stopSimulating', toBackground: true }, '*') - await this.forkProviderPromise if (!this.vnetId) return this.vnetId = undefined - this.publicRpcSlug = undefined + this.publicRpc = undefined this.forkProviderPromise = undefined this.blockNumber = undefined @@ -219,7 +98,9 @@ export class TenderlyProvider extends EventEmitter { } getTransactionLink(txHash: string) { - return `https://dashboard.tenderly.co/explorer/vnet/${this.publicRpcSlug}/tx/${txHash}` + if (!this.publicRpc) return '' + const publicRpcSlug = this.publicRpc.split('/').pop() + return `https://dashboard.tenderly.co/explorer/vnet/${publicRpcSlug}/tx/${txHash}` } private async createFork( @@ -257,25 +138,12 @@ export class TenderlyProvider extends EventEmitter { 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: publicRpc, - }, - '*' - ) + this.publicRpc = json.rpcs.find((rpc: any) => rpc.name === 'Public RPC').url // for requests going directly to Tenderly provider we use the admin RPC so Pilot can fully control the fork - return new JsonRpcProvider(adminRpc) + const provider = new JsonRpcProvider(adminRpc, this.chainId) + + return provider } private increaseBlock = async () => { diff --git a/extension/src/providers/index.ts b/extension/src/providers/index.ts index 8a994749..b9d029a1 100644 --- a/extension/src/providers/index.ts +++ b/extension/src/providers/index.ts @@ -2,8 +2,4 @@ export { default as ForkProvider } from './ForkProvider' export { default as useWalletConnect } from './useWalletConnect' export { getReadOnlyProvider } from './readOnlyProvider' -export { - useTenderlyProvider, - default as ProvideTenderly, -} from './ProvideTenderly' export { ProvideMetaMask, default as useMetaMask } from './useMetaMask' diff --git a/extension/src/providers/readOnlyProvider.ts b/extension/src/providers/readOnlyProvider.ts index 066ebefd..e431e98a 100644 --- a/extension/src/providers/readOnlyProvider.ts +++ b/extension/src/providers/readOnlyProvider.ts @@ -103,13 +103,14 @@ export class Eip1193JsonRpcProvider extends EventEmitter { return toQuantity(result) } + case 'eth_requestAccounts': case 'eth_sendTransaction': case 'eth_signTypedData': case 'eth_signTypedData_v3': case 'eth_signTypedData_v4': case 'personal_sign': case 'eth_sign': { - throw new Error(`${method} requires signing`) + throw new Error(`${method} not supported by read-only provider`) } default: { diff --git a/extension/src/state/reducer.ts b/extension/src/state/reducer.ts index f125c929..a72cc952 100644 --- a/extension/src/state/reducer.ts +++ b/extension/src/state/reducer.ts @@ -5,8 +5,10 @@ import { Action } from './actions' export enum ExecutionStatus { PENDING, SUCCESS, - REVERTED, - MODULE_TRANSACTION_REVERTED, + /** Submitting the transaction failed. This is probably due to an issue in the execution route. */ + FAILED, + /** Submitting the transaction succeeded, but the Safe meta transaction reverted. */ + META_TRANSACTION_REVERTED, } export interface TransactionState { diff --git a/extension/yarn.lock b/extension/yarn.lock index 7e0c2953..8c4b92cd 100644 --- a/extension/yarn.lock +++ b/extension/yarn.lock @@ -1804,6 +1804,18 @@ __metadata: languageName: node linkType: hard +"@safe-global/api-kit@npm:^2.4.4": + version: 2.4.4 + resolution: "@safe-global/api-kit@npm:2.4.4" + dependencies: + "@safe-global/protocol-kit": "npm:^4.0.4" + "@safe-global/safe-core-sdk-types": "npm:^5.0.3" + ethers: "npm:^6.13.1" + node-fetch: "npm:^2.7.0" + checksum: 10/578d1632bfc2f093af3ab89898e39674ba4cb55b42e69ee77c5f5c8976eef00ebc4730fabd53dec22b40a6bcdb72e622454b8fb2149c9abbaa956d1a0df0c82c + languageName: node + linkType: hard + "@safe-global/protocol-kit@npm:^4.0.2": version: 4.0.2 resolution: "@safe-global/protocol-kit@npm:4.0.2" @@ -1819,6 +1831,21 @@ __metadata: languageName: node linkType: hard +"@safe-global/protocol-kit@npm:^4.0.4": + version: 4.0.4 + resolution: "@safe-global/protocol-kit@npm:4.0.4" + dependencies: + "@noble/hashes": "npm:^1.3.3" + "@safe-global/safe-core-sdk-types": "npm:^5.0.3" + "@safe-global/safe-deployments": "npm:^1.37.3" + abitype: "npm:^1.0.2" + ethereumjs-util: "npm:^7.1.5" + ethers: "npm:^6.13.1" + semver: "npm:^7.6.2" + checksum: 10/4acdfa6d2c0ced96084a731d62f96463084ec97a0edd118b455f419e34d9f736e06d8186f56168b7fe4ef9b60eddcf5807710db18db4973ca995ac6e46bdb442 + languageName: node + linkType: hard + "@safe-global/safe-apps-sdk@npm:^9.1.0": version: 9.1.0 resolution: "@safe-global/safe-apps-sdk@npm:9.1.0" @@ -1838,6 +1865,15 @@ __metadata: languageName: node linkType: hard +"@safe-global/safe-core-sdk-types@npm:^5.0.3": + version: 5.0.3 + resolution: "@safe-global/safe-core-sdk-types@npm:5.0.3" + dependencies: + abitype: "npm:^1.0.2" + checksum: 10/e0646c319a7d774ac583f7c589af8a8c18064b4c9d835a19877c2d2edc40ce84b9af47e2fdadd9dd73eec9a7b26c5979b8ad30a80a3c947fb1c2199086068912 + languageName: node + linkType: hard + "@safe-global/safe-deployments@npm:^1.37.0": version: 1.37.1 resolution: "@safe-global/safe-deployments@npm:1.37.1" @@ -1847,10 +1883,19 @@ __metadata: languageName: node linkType: hard -"@safe-global/safe-gateway-typescript-sdk@npm:^3.21.10": - version: 3.21.10 - resolution: "@safe-global/safe-gateway-typescript-sdk@npm:3.21.10" - checksum: 10/602963f3bb8fb96dc8717ba5bf0f1f685d23edcd2c9d14504f633b85e17e09879b1c6ff192fe8877d81bb75a9627fc778f2b49104ed363d1e18074e2f10224de +"@safe-global/safe-deployments@npm:^1.37.3": + version: 1.37.3 + resolution: "@safe-global/safe-deployments@npm:1.37.3" + dependencies: + semver: "npm:^7.6.2" + checksum: 10/3d1fcaac850a1d1100eaa5ff753c88c9bb889c678bb09248323c6b0c9d6228225a88be47973a89bb32829fe2d13ed17c21f854b9fdc29cc1b3c734021761f15c + languageName: node + linkType: hard + +"@safe-global/safe-gateway-typescript-sdk@npm:^3.22.2": + version: 3.22.2 + resolution: "@safe-global/safe-gateway-typescript-sdk@npm:3.22.2" + checksum: 10/7f2b3cab4a1673647c8f7fd927be280f891dc74dba733f302862dee135fedd9d8e1875b1790c75b84c54164b517727bfe08a6dcaf7411659db13eeaefd1407fd languageName: node linkType: hard @@ -10671,11 +10716,11 @@ __metadata: "@eslint/js": "npm:^9.7.0" "@gnosis.pm/zodiac": "npm:^4.0.3" "@noble/hashes": "npm:^1.4.0" - "@safe-global/api-kit": "npm:^2.4.2" - "@safe-global/protocol-kit": "npm:^4.0.2" + "@safe-global/api-kit": "npm:^2.4.4" + "@safe-global/protocol-kit": "npm:^4.0.4" "@safe-global/safe-apps-sdk": "npm:^9.1.0" - "@safe-global/safe-core-sdk-types": "npm:^5.0.2" - "@safe-global/safe-gateway-typescript-sdk": "npm:^3.21.10" + "@safe-global/safe-core-sdk-types": "npm:^5.0.3" + "@safe-global/safe-gateway-typescript-sdk": "npm:^3.22.2" "@shazow/whatsabi": "npm:^0.13.2" "@testing-library/dom": "npm:^10.3.1" "@testing-library/jest-dom": "npm:^6.4.6"