Repository: ethereum/remix-project
Pull request #5884: use ethers.js for estimateGas & fix listening env change
Author: login: yann300
Labels: name: bugfix
Created: 2025-03-17T13:09:58Z | Merged: 2025-03-19T07:23:08Z

bugfix: 
- compatibility with Phantom wallet and universal profile.
- fix a bug introduced by https://github.com/ethereum/remix-project/pull/5844 => when switching from metamask to another browser wallet, the env label isn't updated.


Diff:
diff --git a/apps/remix-ide/src/app/providers/injected-custom-provider.tsx b/apps/remix-ide/src/app/providers/injected-custom-provider.tsx
index 7d90e6423aa..bacc4236785 100644
--- a/apps/remix-ide/src/app/providers/injected-custom-provider.tsx
+++ b/apps/remix-ide/src/app/providers/injected-custom-provider.tsx
@@ -8,15 +8,22 @@ export class InjectedCustomProvider extends InjectedProviderDefault {
   rpcUrls: Array<string>
   nativeCurrency: Record<string, any>
   blockExplorerUrls: Array<string>
+  parent: string
 
-  constructor(provider: any, pluginName: string, chainName: string, chainId: string, rpcUrls: Array<string>, nativeCurrency?: Record<string, any>, blockExplorerUrls?: Array<string>) {
+  constructor(provider: any, pluginName: string, chainName: string, chainId: string, rpcUrls: Array<string>, nativeCurrency?: Record<string, any>, blockExplorerUrls?: Array<string>, parent?: string) {
     super(provider, pluginName)
+    this.parent = parent
     this.pluginName = pluginName
     this.chainName = chainName
     this.chainId = chainId
     this.rpcUrls = rpcUrls
     this.nativeCurrency = nativeCurrency
     this.blockExplorerUrls = blockExplorerUrls
+    this.listenerChainChanged = (chainId: number) => {
+      if (chainId !== parseInt(this.chainId)) {
+        this.call('blockchain', 'changeExecutionContext', { context: this.parent })
+      }
+    }
   }
 
   async init() {
diff --git a/apps/remix-ide/src/app/udapp/run-tab.tsx b/apps/remix-ide/src/app/udapp/run-tab.tsx
index 3ee9bb9c230..f92229735ef 100644
--- a/apps/remix-ide/src/app/udapp/run-tab.tsx
+++ b/apps/remix-ide/src/app/udapp/run-tab.tsx
@@ -16,29 +16,6 @@ import { Recorder } from '../tabs/runTab/model/recorder'
 import { EnvDropdownLabelStateType } from 'libs/remix-ui/run-tab/src/lib/types'
 const _paq = (window._paq = window._paq || [])
 
-const selectedEnvs: EnvDropdownLabelStateType[] = [
-  { name: 'Remix VM (Cancun)', value: 'vm-cancun', chainId: 'vm-cancun' },
-  { name: 'Gnosis Mainnet - MetaMask', value: 'injected-metamask-gnosis', chainId: 100 },
-  { name: 'L2 - Optimism - MetaMask', value: 'injected-metamask-optimism', chainId: 10 },
-  { name: 'L2 - Arbitrum - MetaMask', value: 'injected-metamask-arbitrum', chainId: 42161 },
-  { name: 'Ephemery Testnet - MetaMask', value: 'injected-metamask-ephemery', chainId: 39438143 },
-  { name: 'Sepolia Testnet - MetaMask', value: 'injected-metamask-sepolia', chainId: 11155111 },
-  { name: 'L2 - Linea - MetaMask', value: 'injected-metamask-linea', chainId: 59144 },
-  { name: 'Injected Provider - MetaMask', value: 'injected-MetaMask' },
-  { name: 'WalletConnect', value: 'walletconnect' },
-  { name: 'Remix VM - Mainnet fork', value: 'vm-mainnet-fork', chainId: 'vm-mainnet-fork' },
-  { name: 'Remix VM - Sepolia fork', value: 'vm-sepolia-fork', chainId: 'vm-sepolia-fork' },
-  { name: 'Remix VM - Custom fork', value: 'vm-custom-fork', chainId: 'vm-custom-fork' },
-  { name: 'Remix VM (Shanghai)', value: 'vm-shanghai', chainId: 'vm-shanghai' },
-  { name: 'Remix VM (Paris)', value: 'vm-paris', chainId: 'vm-paris' },
-  { name: 'Remix VM (London)', value: 'vm-london', chainId: 'vm-london' },
-  { name: 'Remix VM (Berlin)', value: 'vm-berlin', chainId: 'vm-berlin' },
-  { name: 'Custom - External Http Provider', value: 'basic-http-provider', chainId: 1741104841094 },
-  { name: 'Dev - Hardhat Provider', value: 'hardhat-provider', chainId: 31337 },
-  { name: 'Dev - Foundry Provider', value: 'foundry-provider', chainId: 31337 },
-  { name: 'Dev - Ganache Provider', value: 'ganache-provider', chainId: 1741104841094 },
-]
-
 const profile = {
   name: 'udapp',
   displayName: 'Deploy & run transactions',
@@ -61,8 +38,7 @@ const profile = {
     'clearAllInstances',
     'addInstance',
     'resolveContractAndAddInstance',
-    'showPluginDetails',
-    'getEnvironmentDropdownLabels'
+    'showPluginDetails'
   ]
 }
 
@@ -79,7 +55,6 @@ export class RunTab extends ViewPlugin {
   fileProvider: any
   recorder: any
   REACT_API: any
-  envDropdownLabelState: EnvDropdownLabelStateType[]
   el: any
   constructor(blockchain: Blockchain, config: any, fileManager: any, editor: any, filePanel: any, compilersArtefacts: CompilerArtefacts, networkModule: any, fileProvider: any, engine: any) {
     super(profile)
@@ -95,7 +70,6 @@ export class RunTab extends ViewPlugin {
     this.fileProvider = fileProvider
     this.recorder = new Recorder(blockchain)
     this.REACT_API = {}
-    this.setEnvironmentDropdownLabels()
     this.setupEvents()
     this.el = document.createElement('div')
   }
@@ -140,14 +114,6 @@ export class RunTab extends ViewPlugin {
     return this.blockchain.createVMAccount(newAccount)
   }
 
-  setEnvironmentDropdownLabels() {
-    this.envDropdownLabelState = selectedEnvs
-  }
-
-  getEnvironmentDropdownLabels() {
-    return this.envDropdownLabelState
-  }
-
   sendTransaction(tx) {
     _paq.push(['trackEvent', 'udapp', 'sendTx', 'udappTransaction'])
     return this.blockchain.sendTransaction(tx)
@@ -242,7 +208,8 @@ export class RunTab extends ViewPlugin {
 
     const addCustomInjectedProvider = async (position, event, name, displayName, networkId, urls, nativeCurrency?) => {
       // name = `${name} through ${event.detail.info.name}`
-      await this.engine.register([new InjectedCustomProvider(event.detail.provider, name, displayName, networkId, urls, nativeCurrency)])
+      const parent = 'injected-' + event.detail.info.name
+      await this.engine.register([new InjectedCustomProvider(event.detail.provider, name, displayName, networkId, urls, nativeCurrency, [], parent)])
       await addProvider(position, name, displayName + ' - ' + event.detail.info.name, { isInjected: true, isVM: false, isRpcForkedState: false, fork: '' })
     }
     const registerInjectedProvider = async (event) => {
diff --git a/apps/remix-ide/src/blockchain/blockchain.tsx b/apps/remix-ide/src/blockchain/blockchain.tsx
index 3a5706dc9c7..74baf5fcbc6 100644
--- a/apps/remix-ide/src/blockchain/blockchain.tsx
+++ b/apps/remix-ide/src/blockchain/blockchain.tsx
@@ -25,7 +25,7 @@ const profile = {
   name: 'blockchain',
   displayName: 'Blockchain',
   description: 'Blockchain - Logic',
-  methods: ['getCode', 'getTransactionReceipt', 'addProvider', 'removeProvider', 'getCurrentFork', 'getAccounts', 'web3VM', 'web3', 'getProvider', 'getCurrentProvider', 'getCurrentNetworkStatus', 'getAllProviders', 'getPinnedProviders'],
+  methods: ['getCode', 'getTransactionReceipt', 'addProvider', 'removeProvider', 'getCurrentFork', 'getAccounts', 'web3VM', 'web3', 'getProvider', 'getCurrentProvider', 'getCurrentNetworkStatus', 'getAllProviders', 'getPinnedProviders', 'changeExecutionContext'],
   version: packageJson.version
 }
 
@@ -545,6 +545,10 @@ export class Blockchain extends Plugin {
   }
 
   changeExecutionContext(context, confirmCb, infoCb, cb) {
+    if (this.currentRequest && this.currentRequest.from && !this.currentRequest.from.startsWith('injected')) {
+      // only injected provider can update the provider.
+      return
+    }
     if (context.context === 'item-another-chain') {
       this.call('manager', 'activatePlugin', 'environmentExplorer').then(() => this.call('tabs', 'focus', 'environmentExplorer'))
     } else {
diff --git a/libs/remix-lib/src/execution/txRunnerWeb3.ts b/libs/remix-lib/src/execution/txRunnerWeb3.ts
index 4b2edaf646b..058726fa40a 100644
--- a/libs/remix-lib/src/execution/txRunnerWeb3.ts
+++ b/libs/remix-lib/src/execution/txRunnerWeb3.ts
@@ -2,7 +2,9 @@
 import { EventManager } from '../eventManager'
 import type { Transaction as InternalTransaction } from './txRunner'
 import { Web3 } from 'web3'
-import { toBigInt, toHex } from 'web3-utils'
+import { toBigInt, toHex, toChecksumAddress } from 'web3-utils'
+import { ethers } from 'ethers'
+import { normalizeHexAddress } from '../helpers/uiHelper'
 
 export class TxRunnerWeb3 {
   event
@@ -37,7 +39,7 @@ export class TxRunnerWeb3 {
     }
 
     let currentDateTime = new Date();
-    const cb = (err, resp) => {
+    const cb = (err, resp, isCreation: boolean) => {
       if (err) {
         return callback(err, resp)
       }
@@ -47,6 +49,18 @@ export class TxRunnerWeb3 {
         return new Promise(async (resolve, reject) => {
           const receipt = await tryTillReceiptAvailable(resp, this.getWeb3())
           tx = await tryTillTxAvailable(resp, this.getWeb3())
+          if (isCreation && !receipt.contractAddress) {
+            // if it is a isCreation, contractAddress should be defined.
+            // if it's not the case look for the event ContractCreated(uint256,address,uint256,bytes32) and extract the address
+            // topic id: 0xa1fb700aaee2ae4a2ff6f91ce7eba292f89c2f5488b8ec4c5c5c8150692595c3
+            if (receipt.logs && receipt.logs.length) {
+              receipt.logs.map((log) => {
+                if (log.topics[0] === '0xa1fb700aaee2ae4a2ff6f91ce7eba292f89c2f5488b8ec4c5c5c8150692595c3') {
+                  (receipt as any).contractAddress = toChecksumAddress(normalizeHexAddress(toHex(log.topics[2])))
+                }
+              })
+            }
+          }
           currentDateTime = new Date();
           resolve({
             receipt,
@@ -58,18 +72,19 @@ export class TxRunnerWeb3 {
       listenOnResponse().then((txData) => { callback(null, txData) }).catch((error) => { callback(error) })
     }
 
+    const isCreation = !tx.to
     if (api.personalMode()) {
       promptCb(
         async (value) => {
           try {
             const res = await (this.getWeb3() as any).eth.personal.sendTransaction({ ...tx, value }, { checkRevertBeforeSending: false, ignoreGasPricing: true })
-            cb(null, res.transactionHash)
+            cb(null, res.transactionHash, isCreation)
           } catch (e) {
             console.log(`Send transaction failed: ${e.message || e.error} . if you use an injected provider, please check it is properly unlocked. `)
             // in case the receipt is available, we consider that only the execution failed but the transaction went through.
             // So we don't consider this to be an error.
-            if (e.receipt) cb(null, e.receipt.transactionHash)
-            else cb(e, null)
+            if (e.receipt) cb(null, e.receipt.transactionHash, isCreation)
+            else cb(e, null, isCreation)
           }
         },
         () => {
@@ -79,7 +94,7 @@ export class TxRunnerWeb3 {
     } else {
       try {
         const res = await this.getWeb3().eth.sendTransaction(tx, null, { checkRevertBeforeSending: false, ignoreGasPricing: true })
-        cb(null, res.transactionHash)
+        cb(null, res.transactionHash, isCreation)
       } catch (e) {
         if (!e.message) e.message = ''
         if (e.error) {
@@ -88,8 +103,8 @@ export class TxRunnerWeb3 {
         console.log(`Send transaction failed: ${e.message} . if you use an injected provider, please check it is properly unlocked. `)
         // in case the receipt is available, we consider that only the execution failed but the transaction went through.
         // So we don't consider this to be an error.
-        if (e.receipt) cb(null, e.receipt.transactionHash)
-        else cb(e, null)
+        if (e.receipt) cb(null, e.receipt.transactionHash, isCreation)
+        else cb(e, null, isCreation)
       }
     }
   }
@@ -134,9 +149,11 @@ export class TxRunnerWeb3 {
           txCopy.gasPrice = undefined
         }
       }
-      this.getWeb3().eth.estimateGas(txCopy)
-        .then(gasEstimation => {
+      const ethersProvider = new ethers.providers.Web3Provider(this.getWeb3().currentProvider as any)
+      ethersProvider.estimateGas(txCopy)
+        .then(gasEstimationBigInt => {
           gasEstimationForceSend(null, () => {
+            const gasEstimation = gasEstimationBigInt.toNumber()
             /*
             * gasLimit is a value that can be set in the UI to hardcap value that can be put in a tx.
             * e.g if the gasestimate
diff --git a/libs/remix-ui/run-tab/src/lib/components/dropdownLabel.tsx b/libs/remix-ui/run-tab/src/lib/components/dropdownLabel.tsx
index c632b4686fa..b03ac2c0785 100644
--- a/libs/remix-ui/run-tab/src/lib/components/dropdownLabel.tsx
+++ b/libs/remix-ui/run-tab/src/lib/components/dropdownLabel.tsx
@@ -1,9 +1,7 @@
-import { Provider } from '@remix-ui/environment-explorer'
 import { CustomTooltip } from '@remix-ui/helper'
 import React, { useEffect, useState } from 'react'
 import { FormattedMessage } from 'react-intl'
-import { EnvDropdownLabelStateType, RunTabState } from '../types'
-import { current } from '@reduxjs/toolkit'
+import { RunTabState } from '../types'
 
 export type DropDownLabelProps = {
   label: string
@@ -21,23 +19,19 @@ export type DropDownLabelProps = {
 export function DropdownLabel({ label, bridges, currentProvider, chainId, runTabState, setExecutionEnv, isL2, plugin }: DropDownLabelProps) {
 
   const [renderLabel, setRenderLabel] = useState(label)
-  const [selectedEnvs, setSelectedEnvs] = useState<EnvDropdownLabelStateType[]>([])
 
   useEffect(() => {
     const checkEnvLabels = async () => {
-      if (selectedEnvs.length === 0) {
-        const envLabels = await plugin.call('udapp', 'getEnvironmentDropdownLabels')
-        setSelectedEnvs(envLabels)
+      const selectedEnvs = await plugin.call('blockchain', 'getAllProviders')
+      const selectedEnv = selectedEnvs[runTabState.selectExEnv]
+      if (selectedEnv) {
+        setRenderLabel(selectedEnv.displayName)
+        setExecutionEnv({ context: selectedEnv.value })
+      } else {
+        setRenderLabel('No provider set')
       }
     }
     checkEnvLabels()
-    const selectedEnv = selectedEnvs.find(env => (env.chainId === chainId && env.value === runTabState.selectExEnv) || (env.value === 'walletconnect' && env.value === currentProvider?.name) || env.chainId === chainId)
-    if (selectedEnv) {
-      setRenderLabel(selectedEnv.name)
-      setExecutionEnv({ context: selectedEnv.value })
-    } else {
-      setRenderLabel('Injected Provider - MetaMask')
-    }
   }, [chainId])
 
   return (


Reviews:
user: login: Aniket-Engg
body: rest code wise, it looks fine
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T16:21:12Z
user: login: yann300
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T17:17:57Z
user: login: yann300
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T17:18:55Z
user: login: Aniket-Engg
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-19T07:23:07Z

Review comments:
diff_hunk: @@ -8,15 +8,22 @@ export class InjectedCustomProvider extends InjectedProviderDefault {
   rpcUrls: Array<string>
   nativeCurrency: Record<string, any>
   blockExplorerUrls: Array<string>
+  parent: string
 
-  constructor(provider: any, pluginName: string, chainName: string, chainId: string, rpcUrls: Array<string>, nativeCurrency?: Record<string, any>, blockExplorerUrls?: Array<string>) {
+  constructor(provider: any, pluginName: string, chainName: string, chainId: string, rpcUrls: Array<string>, nativeCurrency?: Record<string, any>, blockExplorerUrls?: Array<string>, parent?: string) {
     super(provider, pluginName)
+    this.parent = parent
     this.pluginName = pluginName
     this.chainName = chainName
     this.chainId = chainId
     this.rpcUrls = rpcUrls
     this.nativeCurrency = nativeCurrency
     this.blockExplorerUrls = blockExplorerUrls
+    this.listenerChainChanged = (chainId: number) => {
+      if (chainId !== parseInt(this.chainId)) {
path: apps/remix-ide/src/app/providers/injected-custom-provider.tsx
user: login: Aniket-Engg
body: is there a case where this condition will fail?
created_at: 2025-03-18T16:15:09Z
author_association: COLLABORATOR
diff_hunk: @@ -545,6 +545,10 @@ export class Blockchain extends Plugin {
   }
 
   changeExecutionContext(context, confirmCb, infoCb, cb) {
+    if (this.currentRequest && this.currentRequest.from && !this.currentRequest.from.startsWith('injected')) {
path: apps/remix-ide/src/blockchain/blockchain.tsx
user: login: Aniket-Engg
body: why you want to keep this method limited to injected providers?
created_at: 2025-03-18T16:17:55Z
author_association: COLLABORATOR
diff_hunk: @@ -8,15 +8,22 @@ export class InjectedCustomProvider extends InjectedProviderDefault {
   rpcUrls: Array<string>
   nativeCurrency: Record<string, any>
   blockExplorerUrls: Array<string>
+  parent: string
 
-  constructor(provider: any, pluginName: string, chainName: string, chainId: string, rpcUrls: Array<string>, nativeCurrency?: Record<string, any>, blockExplorerUrls?: Array<string>) {
+  constructor(provider: any, pluginName: string, chainName: string, chainId: string, rpcUrls: Array<string>, nativeCurrency?: Record<string, any>, blockExplorerUrls?: Array<string>, parent?: string) {
     super(provider, pluginName)
+    this.parent = parent
     this.pluginName = pluginName
     this.chainName = chainName
     this.chainId = chainId
     this.rpcUrls = rpcUrls
     this.nativeCurrency = nativeCurrency
     this.blockExplorerUrls = blockExplorerUrls
+    this.listenerChainChanged = (chainId: number) => {
+      if (chainId !== parseInt(this.chainId)) {
path: apps/remix-ide/src/app/providers/injected-custom-provider.tsx
user: login: yann300
body: That could happen but just as an edge case.
created_at: 2025-03-18T17:17:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -545,6 +545,10 @@ export class Blockchain extends Plugin {
   }
 
   changeExecutionContext(context, confirmCb, infoCb, cb) {
+    if (this.currentRequest && this.currentRequest.from && !this.currentRequest.from.startsWith('injected')) {
path: apps/remix-ide/src/blockchain/blockchain.tsx
user: login: yann300
body: just to be safe, I don't want all the plugins to be able to update the current provider.
created_at: 2025-03-18T17:18:54Z
author_association: CONTRIBUTOR

PR comments:
user: login: netlify[bot]
created_at: 2025-03-17T13:10:17Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *remixproject* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 8d76521d53b55a760f5dbd2f4b7e2c378c0d7b99 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/remixproject/deploys/67d9ab1ab5cd3a0008ed7c89 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-5884--remixproject.netlify.app](https://deploy-preview-5884--remixproject.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTU4ODQtLXJlbWl4cHJvamVjdC5uZXRsaWZ5LmFwcCJ9.1vmSzO7GuaSloMGjnrYovnZlq3HjScK_TweZ5mIIFr4)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [remixproject Preview](https://deploy-preview-5884--remixproject.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/remixproject/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: ryestew
created_at: 2025-03-17T17:09:45Z
body: This PR was to fix a problem with https://github.com/ethereum/remix-project/pull/5844 but this PR (5884) undid the fix from Joe's PR (5844).  This PR (5884) solves the problem was switching wallets.  

The other PR (5844) was to solve the bug when L2-Optimism-Metamask was selected and then in Metamask a different chain was selected - The environment select box continued to say L-2 Optimism - Metamask.

And a UX problem that was not caught before is that the L-2 Optimism & Arbitrum only work for Metamask. 
![image](https://github.com/user-attachments/assets/bc0a9318-9d6c-4b80-8f1a-359d0774516b)

 But if we have them working for all active wallets , then we will need to have many entries for each like:
L-2 Optimism Raby
L-2 Optimism Metamastk
L-2 Optimism Rainbow

This is an not efficient use of space.
But having the shortcut to a chain is a nice idea!
But having the shortcut (like it currently is) only working with MetaMask - is not the answer nor is having a separate entry for each chain and each wallet.  We need to come up with a new UI solution for shortcuts to configure the connected wallets.

So for the moment, either update the PR to make Joe's fix work or remove the options for L2-optimism & L2-arbitrum.

I just made a separate issue about the shortcuts UI problem that should be addressed outside of this PR (even if this PR will remove the shortcuts for now). https://github.com/ethereum/remix-project/issues/5888
author_association: COLLABORATOR
user: login: ryestew
created_at: 2025-03-18T14:26:59Z
body: I have Rabby Wallet, Rainbow, and Metamask installed.  I choose L2 - Arbitrum and the pulldown says: 
https://github.com/ethereum/remix-project/pull/5884
which is correct.  Then I switch to L2 Optimism and the pulldown does not change - but the Rabby wallet throws a modal saying it switched to Optimism.  So it caught the event ( and seems to have prevented any of the other wallets from receiving the event).

and

When I switch to Injected Provider MetaMask, the pulldown doesn't change to that choice.
When I change the connected chain in MetaMask, that works and updates in the Environment Select box.
But, when I switch from Injected Provider - Rabby to **L2 Optimism - Metamask** - the select box shows the choice but the chain is not correct: 
![image](https://github.com/user-attachments/assets/cdd4fe50-4900-4596-bbae-4be569442267)
(137 is polygon not abritrum).

author_association: COLLABORATOR