From e2c19472ad0f05b8e3c6ea47b83d880196344856 Mon Sep 17 00:00:00 2001 From: Jan-Felix Date: Fri, 31 May 2024 15:57:03 +0200 Subject: [PATCH 01/11] wip --- extension/src/providers/ForkProvider.ts | 8 +-- extension/src/providers/ProvideTenderly.tsx | 57 +++++++++++++++------ 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/extension/src/providers/ForkProvider.ts b/extension/src/providers/ForkProvider.ts index dc36aca7..548b41cd 100644 --- a/extension/src/providers/ForkProvider.ts +++ b/extension/src/providers/ForkProvider.ts @@ -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: 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 } } @@ -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..cca42232 100644 --- a/extension/src/providers/ProvideTenderly.tsx +++ b/extension/src/providers/ProvideTenderly.tsx @@ -10,6 +10,10 @@ import { initSafeProtocolKit } from '../integrations/safe/kits' import { safeInterface } from '../integrations/safe' import { getEip1193ReadOnlyProvider } from './readOnlyProvider' import { ChainId } from '../chains' +import { customAlphabet } from 'nanoid' +import { hexlify } from 'ethers/lib/utils' + +const slug = customAlphabet('abcdefghijklmnopqrstuvwxyz0123456789') const TenderlyContext = React.createContext(null) @@ -172,7 +176,7 @@ export class TenderlyProvider extends EventEmitter { private chainId: number private forkProviderPromise: Promise | undefined - private forkId: string | undefined + private vnetId: string | undefined private transactionIds: Map = new Map() private transactionInfo: Map> = new Map() @@ -185,7 +189,7 @@ export class TenderlyProvider extends EventEmitter { super() this.provider = getEip1193ReadOnlyProvider(chainId, ZERO_ADDRESS) this.chainId = chainId - this.tenderlyForkApi = 'https://fork-api.pilot.gnosisguild.org' + this.tenderlyForkApi = 'https://vnet-api.pilot.gnosisguild.org' this.throttledIncreaseBlock = throttle(this.increaseBlock, 1000) } @@ -267,17 +271,19 @@ export class TenderlyProvider extends EventEmitter { } async deleteFork() { + console.log('DELETE FORK') await this.forkProviderPromise - if (!this.forkId) return + if (!this.vnetId) return // notify the background script to stop intercepting JSON RPC requests window.postMessage({ type: 'stopSimulating', toBackground: true }, '*') - const forkId = this.forkId - this.forkId = undefined + const vnetId = this.vnetId + this.vnetId = undefined this.forkProviderPromise = undefined this.blockNumber = undefined - await fetch(`${this.tenderlyForkApi}/${forkId}`, { + + await fetch(`${this.tenderlyForkApi}/${vnetId}`, { method: 'DELETE', }) } @@ -286,21 +292,40 @@ export class TenderlyProvider extends EventEmitter { networkId: number, blockNumber?: number ): Promise { + console.log({ block_number }) const res = await fetch(this.tenderlyForkApi, { 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, + }, }), }) 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 + this.transactionIds.clear() + const rpcUrl = json.rpcs[0].url // `https://rpc.tenderly.co/fork/${this.vnetId}` + console.log({ rpcUrl }) // notify the background script to start intercepting JSON RPC requests window.postMessage( { type: 'startSimulating', toBackground: true, networkId, rpcUrl }, @@ -312,9 +337,9 @@ export class TenderlyProvider extends EventEmitter { private async fetchForkInfo() { await this.forkProviderPromise - if (!this.forkId) throw new Error('No Tenderly fork available') + if (!this.vnetId) throw new Error('No Tenderly fork available') - const res = await fetch(`${this.tenderlyForkApi}/${this.forkId}`) + const res = await fetch(`${this.tenderlyForkApi}/${this.vnetId}`) const json = await res.json() return json.simulation_fork } @@ -322,13 +347,13 @@ export class TenderlyProvider extends EventEmitter { private async fetchTransactionInfo( transactionHash: string ): Promise { - if (!this.forkId) throw new Error('No Tenderly fork available') + if (!this.vnetId) 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}` + `${this.tenderlyForkApi}/${this.vnetId}/transaction/${transactionId}` ) const json = await res.json() return { From 25778a01dbba83b1ad16981fbd30217996446171 Mon Sep 17 00:00:00 2001 From: Jan-Felix Date: Tue, 4 Jun 2024 17:20:34 +0200 Subject: [PATCH 02/11] wip --- extension/src/background.ts | 19 ++++++++++ extension/src/providers/ProvideTenderly.tsx | 39 +++++++++++---------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/extension/src/background.ts b/extension/src/background.ts index bbe45ff3..450406e8 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) @@ -252,6 +270,7 @@ const detectNetworkOfRpcUrl = async (url: string, tabId: number) => { const result = await networkIdOfRpcUrlPromise.get(url) if (!networkIdOfRpcUrl.has(url)) { networkIdOfRpcUrl.set(url, result) + console.log('lalala', { simulatingExtensionTabs }) console.debug( `detected network of JSON RPC endpoint ${url} in tab #${tabId}: ${result}` ) diff --git a/extension/src/providers/ProvideTenderly.tsx b/extension/src/providers/ProvideTenderly.tsx index cca42232..d1ffba22 100644 --- a/extension/src/providers/ProvideTenderly.tsx +++ b/extension/src/providers/ProvideTenderly.tsx @@ -11,7 +11,6 @@ import { safeInterface } from '../integrations/safe' import { getEip1193ReadOnlyProvider } from './readOnlyProvider' import { ChainId } from '../chains' import { customAlphabet } from 'nanoid' -import { hexlify } from 'ethers/lib/utils' const slug = customAlphabet('abcdefghijklmnopqrstuvwxyz0123456789') @@ -182,14 +181,14 @@ export class TenderlyProvider extends EventEmitter { new Map() 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://vnet-api.pilot.gnosisguild.org' + this.tenderlyVnetApi = 'https://vnet-api.pilot.gnosisguild.org' this.throttledIncreaseBlock = throttle(this.increaseBlock, 1000) } @@ -227,7 +226,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') @@ -271,29 +270,29 @@ export class TenderlyProvider extends EventEmitter { } async deleteFork() { - console.log('DELETE FORK') - await this.forkProviderPromise - if (!this.vnetId) return - // notify the background script to stop intercepting JSON RPC requests window.postMessage({ type: 'stopSimulating', toBackground: true }, '*') - const vnetId = this.vnetId + await this.forkProviderPromise + if (!this.vnetId) return + this.vnetId = undefined this.forkProviderPromise = undefined this.blockNumber = undefined - await fetch(`${this.tenderlyForkApi}/${vnetId}`, { - method: 'DELETE', - }) + // We no longer delete forks/vnets 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', + // }) } private async createFork( networkId: number, blockNumber?: number ): Promise { - console.log({ block_number }) - const res = await fetch(this.tenderlyForkApi, { + console.log({ blockNumber }) + const res = await fetch(this.tenderlyVnetApi, { method: 'POST', body: JSON.stringify({ slug: slug(), @@ -315,6 +314,10 @@ export class TenderlyProvider extends EventEmitter { sync_state_config: { enabled: true, }, + explorer_page_config: { + enabled: true, // enable public explorer page + verification_visibility: 'bytecode', + }, }), }) @@ -337,9 +340,9 @@ export class TenderlyProvider extends EventEmitter { private async fetchForkInfo() { await this.forkProviderPromise - if (!this.vnetId) throw new Error('No Tenderly fork available') + if (!this.vnetId) throw new Error('No Tenderly vnet available') - const res = await fetch(`${this.tenderlyForkApi}/${this.vnetId}`) + const res = await fetch(`${this.tenderlyVnetApi}/${this.vnetId}`) const json = await res.json() return json.simulation_fork } @@ -347,13 +350,13 @@ export class TenderlyProvider extends EventEmitter { private async fetchTransactionInfo( transactionHash: string ): Promise { - if (!this.vnetId) throw new Error('No Tenderly fork available') + if (!this.vnetId) throw new Error('No Tenderly vnet available') const transactionId = this.transactionIds.get(transactionHash) if (!transactionId) throw new Error('Transaction not found') const res = await fetch( - `${this.tenderlyForkApi}/${this.vnetId}/transaction/${transactionId}` + `${this.tenderlyVnetApi}/${this.vnetId}/transaction/${transactionId}` ) const json = await res.json() return { From 3d557ee94192b7e8940e5b46f358226e5ade3960 Mon Sep 17 00:00:00 2001 From: Jan-Felix Date: Thu, 6 Jun 2024 11:44:58 +0200 Subject: [PATCH 03/11] fix tx encoding for tenderly --- extension/src/providers/ForkProvider.ts | 6 +++--- extension/src/providers/ProvideTenderly.tsx | 4 ++-- extension/src/providers/WrappingProvider.ts | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/extension/src/providers/ForkProvider.ts b/extension/src/providers/ForkProvider.ts index 548b41cd..0f4fb391 100644 --- a/extension/src/providers/ForkProvider.ts +++ b/extension/src/providers/ForkProvider.ts @@ -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,7 +249,7 @@ const execTransactionFromModule = ( value: '0x0', from: moduleAddress, // We simulate setting the entire block gas limit as the gas limit for the transaction - gas: hexlify(blockGasLimit / 2), // for some reason tenderly errors when passing the full block gas limit + 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', // doesn't seem to be required } diff --git a/extension/src/providers/ProvideTenderly.tsx b/extension/src/providers/ProvideTenderly.tsx index d1ffba22..fdc56062 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' @@ -10,7 +11,6 @@ import { initSafeProtocolKit } from '../integrations/safe/kits' import { safeInterface } from '../integrations/safe' import { getEip1193ReadOnlyProvider } from './readOnlyProvider' import { ChainId } from '../chains' -import { customAlphabet } from 'nanoid' const slug = customAlphabet('abcdefghijklmnopqrstuvwxyz0123456789') @@ -327,7 +327,7 @@ export class TenderlyProvider extends EventEmitter { this.blockNumber = json.fork_config.block_number this.transactionIds.clear() - const rpcUrl = json.rpcs[0].url // `https://rpc.tenderly.co/fork/${this.vnetId}` + const rpcUrl = json.rpcs.find((rpc: any) => rpc.name === 'Admin RPC').url // `https://rpc.tenderly.co/fork/${this.vnetId}` console.log({ rpcUrl }) // notify the background script to start intercepting JSON RPC requests window.postMessage( diff --git a/extension/src/providers/WrappingProvider.ts b/extension/src/providers/WrappingProvider.ts index a7b1129d..2d071df8 100644 --- a/extension/src/providers/WrappingProvider.ts +++ b/extension/src/providers/WrappingProvider.ts @@ -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 From 051f5c17e2f3ecc29aa50cb70b3226ce99f0cb0a Mon Sep 17 00:00:00 2001 From: Jan-Felix Date: Wed, 26 Jun 2024 15:15:25 +0200 Subject: [PATCH 04/11] fix simulation with vnet --- .../Drawer/SimulatedExecutionCheck.tsx | 80 +++++------ extension/src/providers/ProvideTenderly.tsx | 126 +++--------------- 2 files changed, 63 insertions(+), 143 deletions(-) diff --git a/extension/src/browser/Drawer/SimulatedExecutionCheck.tsx b/extension/src/browser/Drawer/SimulatedExecutionCheck.tsx index a1e321ad..2fb7bcc7 100644 --- a/extension/src/browser/Drawer/SimulatedExecutionCheck.tsx +++ b/extension/src/browser/Drawer/SimulatedExecutionCheck.tsx @@ -4,11 +4,11 @@ import { RiExternalLinkLine, RiGitBranchLine } from 'react-icons/ri' import { Flex, Spinner, Tag } from '../../components' import { useTenderlyProvider } from '../../providers' -import { TenderlyTransactionInfo } from '../../providers/ProvideTenderly' import { useConnection } from '../../connections' import { Connection } from '../../types' import classes from './style.module.css' +import { TransactionReceipt, Web3Provider } from '@ethersproject/providers' enum ExecutionStatus { PENDING, @@ -24,8 +24,9 @@ const SimulatedExecutionCheck: React.FC<{ const tenderlyProvider = useTenderlyProvider() const { connection } = useConnection() - const [transactionInfo, setTransactionInfo] = - useState(null) + const [executionStatus, setExecutionStatus] = useState( + ExecutionStatus.PENDING + ) useEffect(() => { if (!tenderlyProvider) return @@ -33,41 +34,43 @@ const SimulatedExecutionCheck: React.FC<{ let canceled = false - const transactionInfoPromise = - tenderlyProvider.getTransactionInfo(transactionHash) - transactionInfoPromise.then((txInfo) => { - if (!canceled) setTransactionInfo(txInfo) + const provider = new Web3Provider(tenderlyProvider) + provider.getTransactionReceipt(transactionHash).then((receipt) => { + if (canceled) return + + if (!receipt.status) { + setExecutionStatus(ExecutionStatus.REVERTED) + return + } + + if ( + receipt.logs.length === 1 && + isExecutionFromModuleFailure(receipt.logs[0], connection) + ) { + setExecutionStatus(ExecutionStatus.MODULE_TRANSACTION_REVERTED) + } else { + setExecutionStatus(ExecutionStatus.SUCCESS) + } }) return () => { canceled = true } - }, [tenderlyProvider, transactionHash]) - - let executionStatus = ExecutionStatus.PENDING - if (transactionInfo?.status === false) { - executionStatus = ExecutionStatus.REVERTED - } else if (transactionInfo?.status === true) { - if ( - transactionInfo.receipt.logs.length === 1 && - isExecutionFromModuleFailure(transactionInfo.receipt.logs[0], connection) - ) { - executionStatus = ExecutionStatus.MODULE_TRANSACTION_REVERTED - } else { - executionStatus = ExecutionStatus.SUCCESS - } - } + }, [tenderlyProvider, transactionHash, connection]) if (mini) { return ( <> - {!transactionInfo && } color="info">} - {transactionInfo?.status && ( - } color="success"> + {executionStatus === ExecutionStatus.PENDING && ( + } color="info"> )} - {transactionInfo && !transactionInfo.status && ( - } color="danger"> + {executionStatus === ExecutionStatus.SUCCESS && ( + } color="success"> )} + {executionStatus === ExecutionStatus.REVERTED || + (executionStatus === ExecutionStatus.MODULE_TRANSACTION_REVERTED && ( + } color="danger"> + ))} ) } @@ -110,17 +113,16 @@ const SimulatedExecutionCheck: React.FC<{ )} - {transactionInfo && ( - - View in Tenderly - - - )} + + + View in Tenderly + + ) @@ -129,7 +131,7 @@ const SimulatedExecutionCheck: React.FC<{ export default SimulatedExecutionCheck const isExecutionFromModuleFailure = ( - log: TenderlyTransactionInfo['receipt']['logs'][0], + log: TransactionReceipt['logs'][0], connection: Connection ) => { return ( diff --git a/extension/src/providers/ProvideTenderly.tsx b/extension/src/providers/ProvideTenderly.tsx index fdc56062..ff3dfbca 100644 --- a/extension/src/providers/ProvideTenderly.tsx +++ b/extension/src/providers/ProvideTenderly.tsx @@ -124,61 +124,13 @@ 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 vnetId: string | undefined - private transactionIds: Map = new Map() - private transactionInfo: Map> = - new Map() + private publicRpcSlug: string | undefined private blockNumber: number | undefined private tenderlyVnetApi: string @@ -236,35 +188,14 @@ 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 } @@ -277,6 +208,7 @@ export class TenderlyProvider extends EventEmitter { if (!this.vnetId) return this.vnetId = undefined + this.publicRpcSlug = undefined this.forkProviderPromise = undefined this.blockNumber = undefined @@ -287,11 +219,14 @@ export class TenderlyProvider extends EventEmitter { // }) } + getTransactionLink(txHash: string) { + return `https://dashboard.tenderly.co/explorer/vnet/${this.publicRpcSlug}/tx/${txHash}` + } + private async createFork( networkId: number, blockNumber?: number ): Promise { - console.log({ blockNumber }) const res = await fetch(this.tenderlyVnetApi, { method: 'POST', body: JSON.stringify({ @@ -325,44 +260,27 @@ export class TenderlyProvider extends EventEmitter { this.vnetId = json.id this.blockNumber = json.fork_config.block_number - this.transactionIds.clear() - const rpcUrl = json.rpcs.find((rpc: any) => rpc.name === 'Admin RPC').url // `https://rpc.tenderly.co/fork/${this.vnetId}` - console.log({ rpcUrl }) + 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.vnetId) throw new Error('No Tenderly vnet available') - - const res = await fetch(`${this.tenderlyVnetApi}/${this.vnetId}`) - const json = await res.json() - return json.simulation_fork - } - - private async fetchTransactionInfo( - transactionHash: string - ): Promise { - if (!this.vnetId) throw new Error('No Tenderly vnet available') - - const transactionId = this.transactionIds.get(transactionHash) - if (!transactionId) throw new Error('Transaction not found') - - const res = await fetch( - `${this.tenderlyVnetApi}/${this.vnetId}/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 () => { From 166288af3bbc7deda3803582546001ef3c451b4a Mon Sep 17 00:00:00 2001 From: Jan-Felix Date: Wed, 26 Jun 2024 15:35:22 +0200 Subject: [PATCH 05/11] fix role permissions check --- extension/src/utils/decodeError.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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)) From 01552fb72cf532281efef9c88749a8c4d82e1291 Mon Sep 17 00:00:00 2001 From: Jan-Felix Date: Thu, 27 Jun 2024 10:10:44 +0200 Subject: [PATCH 06/11] wip --- extension/src/browser/Drawer/CallContract.tsx | 2 +- extension/src/browser/Drawer/Remove.tsx | 5 ++- extension/src/browser/Drawer/Translate.tsx | 27 +++++------- extension/src/browser/Drawer/index.tsx | 2 +- extension/src/browser/ProvideProvider.tsx | 43 ++++++------------- extension/src/providers/ForkProvider.ts | 2 +- extension/src/providers/WrappingProvider.ts | 2 +- extension/src/state/actions.ts | 25 +++++++---- extension/src/state/reducer.ts | 24 +++++------ extension/src/state/transactionHooks.ts | 2 +- .../src/transactionTranslations/index.ts | 2 +- .../signSnapshotVote.ts | 2 +- .../src/transactionTranslations/types.ts | 2 +- extension/src/types.ts | 11 +++++ .../fetchContractInfo.ts => utils/abi.ts} | 0 15 files changed, 75 insertions(+), 76 deletions(-) create mode 100644 extension/src/types.ts rename extension/src/{browser/fetchContractInfo.ts => utils/abi.ts} (100%) diff --git a/extension/src/browser/Drawer/CallContract.tsx b/extension/src/browser/Drawer/CallContract.tsx index 73d55fb0..58b6ce78 100644 --- a/extension/src/browser/Drawer/CallContract.tsx +++ b/extension/src/browser/Drawer/CallContract.tsx @@ -38,7 +38,7 @@ const CallContract: React.FC = ({ value }) => { {input.name} {input.type} - + ))} diff --git a/extension/src/browser/Drawer/Remove.tsx b/extension/src/browser/Drawer/Remove.tsx index 40373665..07936244 100644 --- a/extension/src/browser/Drawer/Remove.tsx +++ b/extension/src/browser/Drawer/Remove.tsx @@ -29,7 +29,10 @@ export const Remove: React.FC = ({ transaction, index }) => { const laterTransactions = transactions.slice(index + 1) // remove the transaction and all later ones from the store - dispatch({ type: 'REMOVE_TRANSACTION', payload: { id: transaction.id } }) + dispatch({ + type: 'REMOVE_TRANSACTION', + payload: { snapshotId: transaction.snapshotId }, + }) if (transactions.length === 1) { // no more recorded transaction remains: we can delete the fork and will create a fresh one once we receive the next transaction diff --git a/extension/src/browser/Drawer/Translate.tsx b/extension/src/browser/Drawer/Translate.tsx index 3d6cc25d..5fbb6e2f 100644 --- a/extension/src/browser/Drawer/Translate.tsx +++ b/extension/src/browser/Drawer/Translate.tsx @@ -1,37 +1,27 @@ import React from 'react' import { BiWrench } from 'react-icons/bi' -import { encodeSingle, TransactionInput } from 'react-multisend' import { IconButton } from '../../components' import { ForkProvider } from '../../providers' import { useApplicableTranslation } from '../../transactionTranslations' import { useProvider } from '../ProvideProvider' -import { useDispatch, useNewTransactions } from '../../state' +import { TransactionState, useDispatch, useNewTransactions } from '../../state' import classes from './style.module.css' import { encodeTransaction } from '../../encodeTransaction' +import { MetaTransaction } from '../../types' type Props = { - transaction: TransactionInput - isDelegateCall: boolean + transaction: TransactionState index: number labeled?: true } -export const Translate: React.FC = ({ - transaction, - isDelegateCall, - index, - labeled, -}) => { +export const Translate: React.FC = ({ transaction, 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 @@ -48,10 +38,13 @@ export const Translate: React.FC = ({ .map(encodeTransaction) // remove the transaction and all later ones from the store - dispatch({ type: 'REMOVE_TRANSACTION', payload: { id: transaction.id } }) + dispatch({ + type: 'REMOVE_TRANSACTION', + payload: { snapshotId: transaction.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 = transaction.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..c6103247 100644 --- a/extension/src/browser/Drawer/index.tsx +++ b/extension/src/browser/Drawer/index.tsx @@ -46,7 +46,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)) { diff --git a/extension/src/browser/ProvideProvider.tsx b/extension/src/browser/ProvideProvider.tsx index 840b1806..fc8fed8e 100644 --- a/extension/src/browser/ProvideProvider.tsx +++ b/extension/src/browser/ProvideProvider.tsx @@ -16,9 +16,9 @@ import { import { useConnection } from '../connections' import { Eip1193Provider } from '../types' -import { fetchContractInfo } from './fetchContractInfo' import { useDispatch, useNewTransactions } from '../state' import { encodeTransaction } from '../encodeTransaction' +import { fetchContractInfo } from '../utils/abi' interface Props { simulate: boolean @@ -52,52 +52,37 @@ 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) { 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 + 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, }, }) }, }), - [tenderlyProvider, provider, connection, dispatch] + [tenderlyProvider, connection, dispatch] ) const submitTransactions = useCallback(async () => { diff --git a/extension/src/providers/ForkProvider.ts b/extension/src/providers/ForkProvider.ts index 0f4fb391..3b6deb4e 100644 --- a/extension/src/providers/ForkProvider.ts +++ b/extension/src/providers/ForkProvider.ts @@ -2,7 +2,7 @@ import EventEmitter from 'events' import { ContractFactories, KnownContracts } from '@gnosis.pm/zodiac' import { BigNumber, ethers } from 'ethers' -import { MetaTransaction } from 'react-multisend' +import { MetaTransaction } from '../../types' import { TransactionOptions } from '@safe-global/safe-core-sdk-types' import { generatePreValidatedSignature } from '@safe-global/protocol-kit/dist/src/utils' diff --git a/extension/src/providers/WrappingProvider.ts b/extension/src/providers/WrappingProvider.ts index 2d071df8..63c842f1 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 '../types' import { initSafeApiKit, sendTransaction } from '../integrations/safe' import { Connection, Eip1193Provider, TransactionData } from '../types' diff --git a/extension/src/state/actions.ts b/extension/src/state/actions.ts index 9048c792..76a5098b 100644 --- a/extension/src/state/actions.ts +++ b/extension/src/state/actions.ts @@ -1,18 +1,25 @@ -import { TransactionInput } from 'react-multisend' +import { MetaTransaction } from '../types' -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: MetaTransaction + } } interface ConfirmTransactionAction { type: 'CONFIRM_TRANSACTION' payload: { - id: string + snapshotId: string transactionHash: string } } @@ -20,14 +27,14 @@ interface ConfirmTransactionAction { interface RemoveTransactionAction { type: 'REMOVE_TRANSACTION' payload: { - id: string + snapshotId: string } } interface RemoveTransactionAction { type: 'REMOVE_TRANSACTION' payload: { - id: string + snapshotId: string } } @@ -46,7 +53,7 @@ interface ClearTransactionsAction { } export type Action = - | AppendRawTransactionAction + | AppendTransactionAction | DecodeTransactionAction | ConfirmTransactionAction | RemoveTransactionAction diff --git a/extension/src/state/reducer.ts b/extension/src/state/reducer.ts index ed4171b8..9abfc240 100644 --- a/extension/src/state/reducer.ts +++ b/extension/src/state/reducer.ts @@ -1,10 +1,10 @@ -import { TransactionInput } from 'react-multisend' +import { MetaTransaction } from '../types' import { Action } from './actions' export interface TransactionState { - input: TransactionInput - isDelegateCall: boolean + snapshotId: string + transaction: MetaTransaction transactionHash?: string batchTransactionHash?: string } @@ -14,30 +14,30 @@ const rootReducer = ( action: Action ): TransactionState[] => { switch (action.type) { - case 'APPEND_RAW_TRANSACTION': { - const { input, isDelegateCall } = action.payload - return [...state, { input, isDelegateCall }] + case 'APPEND_RANSACTION': { + const { snapshotId, transaction } = action.payload + return [...state, { snapshotId, transaction }] } 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.input.id === id ? { ...item, transactionHash } : item + item.snapshotId === snapshotId ? { ...item, transactionHash } : 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..29d71558 100644 --- a/extension/src/transactionTranslations/index.ts +++ b/extension/src/transactionTranslations/index.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { MetaTransaction } from 'react-multisend' +import { MetaTransaction } from '../types' import { ChainId } from '../chains' import { useConnection } from '../connections' diff --git a/extension/src/transactionTranslations/signSnapshotVote.ts b/extension/src/transactionTranslations/signSnapshotVote.ts index 186b6cc9..092e924e 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 '../types' // 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..8ca45326 100644 --- a/extension/src/transactionTranslations/types.ts +++ b/extension/src/transactionTranslations/types.ts @@ -1,4 +1,4 @@ -import { MetaTransaction } from 'react-multisend' +import { MetaTransaction } from '../types' import { ChainId } from '../chains' import { SupportedModuleType } from '../integrations/zodiac/types' diff --git a/extension/src/types.ts b/extension/src/types.ts new file mode 100644 index 00000000..463239fb --- /dev/null +++ b/extension/src/types.ts @@ -0,0 +1,11 @@ +export enum OperationType { + Call = 0, + DelegateCall = 1, +} + +export interface MetaTransaction { + readonly to: string + readonly value: string + readonly data: string + readonly operation?: OperationType +} 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 From 6c4cd9374e670a0494f3b3b02d916fcd622e8ba0 Mon Sep 17 00:00:00 2001 From: Jan-Felix Date: Fri, 28 Jun 2024 15:50:50 +0200 Subject: [PATCH 07/11] refactor: remove react-multisend dep --- extension/.pnp.cjs | 32 +--- ...tisend-npm-2.1.0-5385f450b0-0603640ffe.zip | Bin 39923 -> 0 bytes extension/package.json | 2 +- extension/src/browser/Drawer/CallContract.tsx | 51 ------ .../browser/Drawer/ContractAddress/index.tsx | 27 +--- .../src/browser/Drawer/CopyToClipboard.tsx | 20 +-- .../src/browser/Drawer/DecodedTransaction.tsx | 32 ++++ .../src/browser/Drawer/RawTransaction.tsx | 7 +- extension/src/browser/Drawer/Remove.tsx | 20 ++- .../browser/Drawer/RolePermissionCheck.tsx | 37 ++--- .../Drawer/SimulatedExecutionCheck.tsx | 144 ----------------- .../src/browser/Drawer/SimulationStatus.tsx | 91 +++++++++++ extension/src/browser/Drawer/Transaction.tsx | 153 ++++++------------ extension/src/browser/Drawer/Translate.tsx | 16 +- extension/src/browser/Drawer/index.tsx | 22 +-- .../browser/Drawer/useDecodedFunctionData.tsx | 33 ++++ extension/src/browser/ProvideProvider.tsx | 63 +++++++- extension/src/encodeTransaction.ts | 9 -- .../src/integrations/safe/sendTransaction.ts | 5 +- extension/src/providers/ForkProvider.ts | 2 +- extension/src/providers/WrappingProvider.ts | 2 +- extension/src/state/actions.ts | 15 +- extension/src/state/reducer.ts | 27 +++- extension/src/types.ts | 11 -- extension/yarn.lock | 20 +-- 25 files changed, 361 insertions(+), 480 deletions(-) delete mode 100644 extension/.yarn/cache/react-multisend-npm-2.1.0-5385f450b0-0603640ffe.zip delete mode 100644 extension/src/browser/Drawer/CallContract.tsx create mode 100644 extension/src/browser/Drawer/DecodedTransaction.tsx delete mode 100644 extension/src/browser/Drawer/SimulatedExecutionCheck.tsx create mode 100644 extension/src/browser/Drawer/SimulationStatus.tsx create mode 100644 extension/src/browser/Drawer/useDecodedFunctionData.tsx delete mode 100644 extension/src/encodeTransaction.ts delete mode 100644 extension/src/types.ts 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 b32970ea355fb753c70d551d0f65846b879ac688..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39923 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

Gp?i$2xE6^;@cX#8olAa9ICFiF5Ru*??>t`*W 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 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/browser/Drawer/CallContract.tsx b/extension/src/browser/Drawer/CallContract.tsx deleted file mode 100644 index 58b6ce78..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, index, labeled }) => { +export const Translate: React.FC = ({ + transactionState, + index, + labeled, +}) => { const provider = useProvider() const dispatch = useDispatch() const transactions = useNewTransactions() @@ -35,16 +37,16 @@ export const Translate: React.FC = ({ transaction, index, labeled }) => { 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: { snapshotId: transaction.snapshotId }, + payload: { snapshotId: transactionState.snapshotId }, }) // revert to checkpoint before the transaction to remove - const checkpoint = transaction.transactionHash // 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 c6103247..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) @@ -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 fc8fed8e..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 { 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 @@ -53,12 +53,13 @@ const ProvideProvider: React.FC = ({ simulate, children }) => { ownerAddress: connection.pilotAddress, 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 for real and we update the state once that's done. + // 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 @@ -80,13 +81,48 @@ const ProvideProvider: React.FC = ({ simulate, children }) => { 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, connection, dispatch] ) const submitTransactions = useCallback(async () => { - const metaTransactions = transactions.map(encodeTransaction) + const metaTransactions = transactions.map((txState) => txState.transaction) console.log( transactions.length === 1 @@ -127,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/providers/ForkProvider.ts b/extension/src/providers/ForkProvider.ts index 3b6deb4e..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 '../../types' 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' diff --git a/extension/src/providers/WrappingProvider.ts b/extension/src/providers/WrappingProvider.ts index 63c842f1..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 '../types' +import { MetaTransaction } from 'ethers-multisend' import { initSafeApiKit, sendTransaction } from '../integrations/safe' import { Connection, Eip1193Provider, TransactionData } from '../types' diff --git a/extension/src/state/actions.ts b/extension/src/state/actions.ts index 76a5098b..4a90f0ef 100644 --- a/extension/src/state/actions.ts +++ b/extension/src/state/actions.ts @@ -1,4 +1,6 @@ -import { MetaTransaction } from '../types' +import { MetaTransaction } from 'ethers-multisend' +import { ContractInfo } from '../utils/abi' +import { ExecutionStatus } from './reducer' interface AppendTransactionAction { type: 'APPEND_TRANSACTION' @@ -12,7 +14,7 @@ interface DecodeTransactionAction { type: 'DECODE_TRANSACTION' payload: { snapshotId: string - contractInfo: MetaTransaction + contractInfo: ContractInfo } } @@ -24,6 +26,14 @@ interface ConfirmTransactionAction { } } +interface UpdateTransactionStatusAction { + type: 'UPDATE_TRANSACTION_STATUS' + payload: { + snapshotId: string + status: ExecutionStatus + } +} + interface RemoveTransactionAction { type: 'REMOVE_TRANSACTION' payload: { @@ -56,6 +66,7 @@ export type Action = | AppendTransactionAction | DecodeTransactionAction | ConfirmTransactionAction + | UpdateTransactionStatusAction | RemoveTransactionAction | SubmitTransactionsAction | ClearTransactionsAction diff --git a/extension/src/state/reducer.ts b/extension/src/state/reducer.ts index 9abfc240..4609a6f5 100644 --- a/extension/src/state/reducer.ts +++ b/extension/src/state/reducer.ts @@ -1,10 +1,19 @@ -import { MetaTransaction } from '../types' - +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 { snapshotId: string transaction: MetaTransaction + status: ExecutionStatus + contractInfo?: ContractInfo transactionHash?: string batchTransactionHash?: string } @@ -14,9 +23,12 @@ const rootReducer = ( action: Action ): TransactionState[] => { switch (action.type) { - case 'APPEND_RANSACTION': { + case 'APPEND_TRANSACTION': { const { snapshotId, transaction } = action.payload - return [...state, { snapshotId, transaction }] + return [ + ...state, + { snapshotId, transaction, status: ExecutionStatus.PENDING }, + ] } case 'DECODE_TRANSACTION': { @@ -33,6 +45,13 @@ const rootReducer = ( ) } + case 'UPDATE_TRANSACTION_STATUS': { + const { snapshotId, status } = action.payload + return state.map((item) => + item.snapshotId === snapshotId ? { ...item, status } : item + ) + } + case 'REMOVE_TRANSACTION': { const { snapshotId } = action.payload return state.slice( diff --git a/extension/src/types.ts b/extension/src/types.ts deleted file mode 100644 index 463239fb..00000000 --- a/extension/src/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -export enum OperationType { - Call = 0, - DelegateCall = 1, -} - -export interface MetaTransaction { - readonly to: string - readonly value: string - readonly data: string - readonly operation?: OperationType -} 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 From 236eeb47eceffd502faf1ca93467b0631cf17a72 Mon Sep 17 00:00:00 2001 From: Jan-Felix Date: Mon, 1 Jul 2024 09:31:06 +0200 Subject: [PATCH 08/11] fix imports --- extension/src/integrations/safe/signing.ts | 2 +- extension/src/transactionTranslations/index.ts | 2 +- extension/src/transactionTranslations/signSnapshotVote.ts | 2 +- extension/src/transactionTranslations/types.ts | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) 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/transactionTranslations/index.ts b/extension/src/transactionTranslations/index.ts index 29d71558..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 '../types' import { ChainId } from '../chains' import { useConnection } from '../connections' diff --git a/extension/src/transactionTranslations/signSnapshotVote.ts b/extension/src/transactionTranslations/signSnapshotVote.ts index 092e924e..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 '../types' +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 8ca45326..11e36588 100644 --- a/extension/src/transactionTranslations/types.ts +++ b/extension/src/transactionTranslations/types.ts @@ -1,5 +1,4 @@ -import { MetaTransaction } from '../types' - +import { MetaTransaction } from 'ethers-multisend' import { ChainId } from '../chains' import { SupportedModuleType } from '../integrations/zodiac/types' From f5e544c1ffd447e41a9e06e79c5c640877ae4452 Mon Sep 17 00:00:00 2001 From: Jan-Felix Date: Mon, 1 Jul 2024 09:35:39 +0200 Subject: [PATCH 09/11] fix bug --- extension/src/browser/Drawer/RolePermissionCheck.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extension/src/browser/Drawer/RolePermissionCheck.tsx b/extension/src/browser/Drawer/RolePermissionCheck.tsx index dcdc3035..7482d656 100644 --- a/extension/src/browser/Drawer/RolePermissionCheck.tsx +++ b/extension/src/browser/Drawer/RolePermissionCheck.tsx @@ -71,7 +71,9 @@ const RolePermissionCheck: React.FC<{ const { connection } = useConnection() const tenderlyProvider = useTenderlyProvider() - const translationAvailable = !!useApplicableTranslation(transactionState) + const translationAvailable = !!useApplicableTranslation( + transactionState.transaction + ) useEffect(() => { let canceled = false From 38132f5f19a2d65360d73ae4dddf8280aa4e56f1 Mon Sep 17 00:00:00 2001 From: Jan-Felix Date: Mon, 1 Jul 2024 09:39:33 +0200 Subject: [PATCH 10/11] appease spell checker --- extension/.cspell.json | 2 ++ extension/src/providers/ProvideTenderly.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) 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/src/providers/ProvideTenderly.tsx b/extension/src/providers/ProvideTenderly.tsx index ff3dfbca..9ce9cc66 100644 --- a/extension/src/providers/ProvideTenderly.tsx +++ b/extension/src/providers/ProvideTenderly.tsx @@ -212,7 +212,7 @@ export class TenderlyProvider extends EventEmitter { this.forkProviderPromise = undefined this.blockNumber = undefined - // We no longer delete forks/vnets on Tenderly. That way we will be able to persist and share Pilot sessions in the future. + // 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', From f2517084a4849c8019548ae3ca9b56d03facea0b Mon Sep 17 00:00:00 2001 From: Jan-Felix Date: Mon, 1 Jul 2024 09:40:07 +0200 Subject: [PATCH 11/11] Update background.ts --- extension/src/background.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/extension/src/background.ts b/extension/src/background.ts index 450406e8..625e71d4 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -270,7 +270,6 @@ const detectNetworkOfRpcUrl = async (url: string, tabId: number) => { const result = await networkIdOfRpcUrlPromise.get(url) if (!networkIdOfRpcUrl.has(url)) { networkIdOfRpcUrl.set(url, result) - console.log('lalala', { simulatingExtensionTabs }) console.debug( `detected network of JSON RPC endpoint ${url} in tab #${tabId}: ${result}` )