diff --git a/apps/remix-ide-e2e/src/tests/proxy_oz_v5_non_shanghai_runtime.test.ts b/apps/remix-ide-e2e/src/tests/proxy_oz_v5_non_shanghai_runtime.test.ts new file mode 100644 index 0000000000..a93c9afc51 --- /dev/null +++ b/apps/remix-ide-e2e/src/tests/proxy_oz_v5_non_shanghai_runtime.test.ts @@ -0,0 +1,353 @@ +'use strict' +import { NightwatchBrowser } from 'nightwatch' +import init from '../helpers/init' + +let firstProxyAddress: string +let lastProxyAddress: string +let shortenedFirstAddress: string +let shortenedLastAddress: string +module.exports = { + '@disabled': true, + before: function (browser: NightwatchBrowser, done: VoidFunction) { + init(browser, done) + }, + + '@sources': function () { + return sources + }, + + + 'Should show deploy proxy option for UUPS upgradeable contract #group1': function (browser: NightwatchBrowser) { + browser + .clickLaunchIcon('udapp') + .switchEnvironment('vm-merge') // this runtime doesn't have the PUSH0 opcode. + .clickLaunchIcon('solidity') + .click('.remixui_compilerConfigSection') + .setValue('#evmVersionSelector', 'paris') // set an evm version which doesn't have PUSH0 opcode. + .clickLaunchIcon('filePanel') + .addFile('myTokenV1.sol', sources[0]['myTokenV1.sol']) + .clickLaunchIcon('solidity') + .pause(2000) + // because the compilatiom imports are slow and sometimes stop loading (not sure why, it's bug) we need to recompile and check to see if the files are really in de FS + .click('[data-id="compilerContainerCompileBtn"]') + .clickLaunchIcon('filePanel') + .isVisible({ + selector: '*[data-id="treeViewDivtreeViewItem.deps/npm/@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"]', + timeout: 120000, + suppressNotFoundErrors: true + }) + .clickLaunchIcon('solidity') + .click('[data-id="compilerContainerCompileBtn"]') + .clickLaunchIcon('filePanel') + .isVisible({ + selector: '*[data-id="treeViewDivtreeViewItem.deps/npm/@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"]', + timeout: 120000, + suppressNotFoundErrors: true + }) + .clickLaunchIcon('solidity') + .click('[data-id="compilerContainerCompileBtn"]') + .clickLaunchIcon('filePanel') + .waitForElementVisible({ + selector: '*[data-id="treeViewDivtreeViewItem.deps/npm/@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"]', + timeout: 120000, + }) + .clickLaunchIcon('solidity') + .waitForElementPresent('select[id="compiledContracts"] option[value=MyToken]', 60000) + .clickLaunchIcon('udapp') + .click('select.udapp_contractNames') + .click('select.udapp_contractNames option[value=MyToken]') + .waitForElementPresent('[data-id="contractGUIDeployWithProxyLabel"]') + .waitForElementPresent('[data-id="contractGUIUpgradeImplementationLabel"]') + }, + + 'Should show upgrade proxy option for child contract inheriting UUPS parent contract #group1': function (browser: NightwatchBrowser) { + browser + .addFile('myTokenV2.sol', sources[1]['myTokenV2.sol']) + .clickLaunchIcon('solidity') + .assert.visible('[data-id="compilerContainerCompileBtn"]') + .click('[data-id="compilerContainerCompileBtn"]') + .waitForElementPresent('select[id="compiledContracts"] option[value=MyTokenV2]', 60000) + .clickLaunchIcon('udapp') + .click('select.udapp_contractNames') + .click('select.udapp_contractNames option[value=MyTokenV2]') + .waitForElementPresent('[data-id="contractGUIDeployWithProxyLabel"]') + .waitForElementPresent('[data-id="contractGUIUpgradeImplementationLabel"]') + }, + + 'Should deploy proxy without initialize parameters #group1': function (browser: NightwatchBrowser) { + browser + .openFile('myTokenV1.sol') + .clickLaunchIcon('solidity') + .assert.visible('[data-id="compilerContainerCompileBtn"]') + .click('[data-id="compilerContainerCompileBtn"]') + .waitForElementPresent('select[id="compiledContracts"] option[value=MyToken]', 60000) + .clickLaunchIcon('udapp') + .click('select.udapp_contractNames') + .click('select.udapp_contractNames option[value=MyToken]') + .verify.visible('[data-id="contractGUIDeployWithProxyLabel"]') + .waitForElementPresent('[data-id="contractGUIDeployWithProxyLabel"]') + .click('[data-id="contractGUIDeployWithProxyLabel"]') + .setValue('[data-id="initializeInputs-initialOwner"]', '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4') + .createContract('') + .waitForElementContainsText('[data-id="udappNotifyModalDialogModalTitle-react"]', 'Deploy Implementation & Proxy (ERC1967)') + .waitForElementVisible('[data-id="udappNotify-modal-footer-ok-react"]') + .click('[data-id="udappNotify-modal-footer-ok-react"]') + .waitForElementContainsText('[data-id="confirmProxyDeploymentModalDialogModalTitle-react"]', 'Confirm Deploy Proxy (ERC1967)') + .waitForElementVisible('[data-id="confirmProxyDeployment-modal-footer-ok-react"]') + .click('[data-id="confirmProxyDeployment-modal-footer-ok-react"]') + .waitForElementPresent('[data-id="universalDappUiTitleExpander0"]') + .waitForElementPresent('[data-id="universalDappUiTitleExpander1"]') + .waitForElementContainsText('*[data-id="terminalJournal"]', 'Deploying ERC1967 >= 5.0.0 as proxy...') + }, + + 'Should interact with deployed contract via ERC1967 (proxy) #group1': function (browser: NightwatchBrowser) { + browser + .getAddressAtPosition(1, (address) => { + firstProxyAddress = address + shortenedFirstAddress = address.slice(0, 5) + '...' + address.slice(address.length - 5, address.length) + }) + .clickInstance(1) + .perform((done) => { + browser.testConstantFunction(firstProxyAddress, 'name - call', null, '0:\nstring: MyToken').perform(() => { + done() + }) + }) + .perform((done) => { + browser.testConstantFunction(firstProxyAddress, 'symbol - call', null, '0:\nstring: MTK').perform(() => { + done() + }) + }) + }, + + 'Should deploy proxy with initialize parameters #group1': function (browser: NightwatchBrowser) { + browser + .waitForElementPresent('[data-id="deployAndRunClearInstances"]') + .click('[data-id="deployAndRunClearInstances"]') + .addFile('initializeProxy.sol', sources[2]['initializeProxy.sol']) + .clickLaunchIcon('solidity') + .assert.visible('[data-id="compilerContainerCompileBtn"]') + .click('[data-id="compilerContainerCompileBtn"]') + .waitForElementPresent('select[id="compiledContracts"] option[value=MyInitializedToken]', 60000) + .clickLaunchIcon('udapp') + .click('select.udapp_contractNames') + .click('select.udapp_contractNames option[value=MyInitializedToken]') + .waitForElementPresent('[data-id="contractGUIDeployWithProxyLabel"]') + .click('[data-id="contractGUIDeployWithProxyLabel"]') + .useXpath() + .waitForElementPresent('//*[@id="runTabView"]/div/div[2]/div[3]/div[1]/div/div[1]/div[4]/div/div[1]/input') + .waitForElementPresent('//*[@id="runTabView"]/div/div[2]/div[3]/div[1]/div/div[1]/div[4]/div/div[2]/input') + .setValue('//*[@id="runTabView"]/div/div[2]/div[3]/div[1]/div/div[1]/div[4]/div/div[1]/input', 'Remix') + .setValue('//*[@id="runTabView"]/div/div[2]/div[3]/div[1]/div/div[1]/div[4]/div/div[2]/input', "R") + .useCss() + .setValue('[data-id="initializeInputs-initialOwner"]', '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4') + .createContract('') + .waitForElementContainsText('[data-id="udappNotifyModalDialogModalTitle-react"]', 'Deploy Implementation & Proxy (ERC1967)') + .waitForElementVisible('[data-id="udappNotify-modal-footer-ok-react"]') + .click('[data-id="udappNotify-modal-footer-ok-react"]') + .waitForElementContainsText('[data-id="confirmProxyDeploymentModalDialogModalTitle-react"]', 'Confirm Deploy Proxy (ERC1967)') + .waitForElementVisible('[data-id="confirmProxyDeployment-modal-footer-ok-react"]') + .click('[data-id="confirmProxyDeployment-modal-footer-ok-react"]') + .waitForElementPresent('[data-id="universalDappUiTitleExpander0"]') + .waitForElementPresent('[data-id="universalDappUiTitleExpander1"]') + .waitForElementContainsText('*[data-id="terminalJournal"]', 'Deploying ERC1967 >= 5.0.0 as proxy...') + }, + + 'Should interact with initialized contract to verify parameters #group1': function (browser: NightwatchBrowser) { + browser + .getAddressAtPosition(1, (address) => { + lastProxyAddress = address + shortenedLastAddress = address.slice(0, 5) + '...' + address.slice(address.length - 5, address.length) + }) + .clickInstance(1) + .perform((done) => { + browser.testConstantFunction(lastProxyAddress, 'name - call', null, '0:\nstring: Remix').perform(() => { + done() + }) + }) + .perform((done) => { + browser.testConstantFunction(lastProxyAddress, 'symbol - call', null, '0:\nstring: R').perform(() => { + done() + }) + }) + }, + + 'Should upgrade contract by selecting a previously deployed proxy address from dropdown (MyTokenV1 to MyTokenV2) #group1': function (browser: NightwatchBrowser) { + browser + .click('*[data-id="terminalClearConsole"]') + .waitForElementPresent('[data-id="deployAndRunClearInstances"]') + .click('[data-id="deployAndRunClearInstances"]') + .openFile('myTokenV2.sol') + .clickLaunchIcon('solidity') + .assert.visible('[data-id="compilerContainerCompileBtn"]') + .click('[data-id="compilerContainerCompileBtn"]') + .waitForElementPresent('select[id="compiledContracts"] option[value=MyTokenV2]', 60000) + .clickLaunchIcon('udapp') + .click('select.udapp_contractNames') + .click('select.udapp_contractNames option[value=MyTokenV2]') + .waitForElementPresent('[data-id="contractGUIUpgradeImplementationLabel"]') + .click('[data-id="contractGUIUpgradeImplementationLabel"]') + .waitForElementPresent('[data-id="toggleProxyAddressDropdown"]') + .click('[data-id="toggleProxyAddressDropdown"]') + .waitForElementVisible('[data-id="proxy-dropdown-items"]') + .assert.textContains('[data-id="proxy-dropdown-items"]', shortenedFirstAddress) + .assert.textContains('[data-id="proxy-dropdown-items"]', shortenedLastAddress) + + .click('[data-id="proxyAddress1"]') + .createContract('') + .waitForElementContainsText('[data-id="udappNotifyModalDialogModalTitle-react"]', 'Deploy Implementation & Update Proxy') + .waitForElementVisible('[data-id="udappNotify-modal-footer-ok-react"]') + .click('[data-id="udappNotify-modal-footer-ok-react"]') + .waitForElementContainsText('[data-id="confirmProxyDeploymentModalDialogModalTitle-react"]', 'Confirm Update Proxy (ERC1967)') + .waitForElementVisible('[data-id="confirmProxyDeployment-modal-footer-ok-react"]') + .click( + { + selector: '[data-id="confirmProxyDeployment-modal-footer-ok-react"]', + }) + .waitForElementPresent('[data-id="universalDappUiTitleExpander0"]') + .waitForElementPresent('[data-id="universalDappUiTitleExpander1"]') + .waitForElementContainsText('*[data-id="terminalJournal"]', 'Using ERC1967 >= 5.0.0 for the proxy upgrade...') + }, + + 'Should interact with upgraded function in contract MyTokenV2 #group1': function (browser: NightwatchBrowser) { + browser + .clickInstance(1) + .perform((done) => { + browser.testConstantFunction(lastProxyAddress, 'version - call', null, '0:\nstring: MyTokenV2!').perform(() => { + done() + }) + }) + }, + + 'Should upgrade contract by providing proxy address in input field (MyTokenV1 to MyTokenV2) #group1': function (browser: NightwatchBrowser) { + browser + .click('*[data-id="terminalClearConsole"]') + .waitForElementPresent('[data-id="deployAndRunClearInstances"]') + .click('[data-id="deployAndRunClearInstances"]') + .openFile('myTokenV2.sol') + .clickLaunchIcon('solidity') + .assert.visible('[data-id="compilerContainerCompileBtn"]') + .click('[data-id="compilerContainerCompileBtn"]') + .waitForElementPresent('select[id="compiledContracts"] option[value=MyTokenV2]', 60000) + .clickLaunchIcon('udapp') + .click('select.udapp_contractNames') + .click('select.udapp_contractNames option[value=MyTokenV2]') + .waitForElementPresent('[data-id="contractGUIUpgradeImplementationLabel"]') + .waitForElementPresent('[data-id="toggleProxyAddressDropdown"]') + .clearValue('[data-id="ERC1967AddressInput"]') + .setValue('[data-id="ERC1967AddressInput"]', firstProxyAddress) + .createContract('') + .waitForElementContainsText('[data-id="udappNotifyModalDialogModalTitle-react"]', 'Deploy Implementation & Update Proxy') + .waitForElementVisible('[data-id="udappNotify-modal-footer-ok-react"]') + .click('[data-id="udappNotify-modal-footer-ok-react"]') + .waitForElementContainsText('[data-id="confirmProxyDeploymentModalDialogModalTitle-react"]', 'Confirm Update Proxy (ERC1967)') + .waitForElementVisible('[data-id="confirmProxyDeployment-modal-footer-ok-react"]') + .click('[data-id="confirmProxyDeployment-modal-footer-ok-react"]') + .waitForElementPresent('[data-id="universalDappUiTitleExpander0"]') + .waitForElementPresent('[data-id="universalDappUiTitleExpander1"]') + .waitForElementContainsText('*[data-id="terminalJournal"]', 'Using ERC1967 >= 5.0.0 for the proxy upgrade...') + }, + + 'Should interact with upgraded contract through provided proxy address #group1': function (browser: NightwatchBrowser) { + browser + .clearConsole() + .clickInstance(1) + .perform((done) => { + browser.testConstantFunction(firstProxyAddress, 'version - call', null, '0:\nstring: MyTokenV2!').perform(() => { + done() + }) + }) + }, + 'Should debug the call': function(browser: NightwatchBrowser) { + browser + .debugTransaction(0) + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: '//*[@data-id="treeViewLivm trace step" and contains(.,"7")]', + timeout: 60000 + }) + .goToVMTraceStep(129) + .waitForElementContainsText('*[data-id="functionPanel"]', 'version()', 60000) + .end() + } +} + + +const sources = [ + { + 'myTokenV1.sol': { + content: ` + // SPDX-License-Identifier: MIT + pragma solidity ^0.8.20; + + import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; + import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + + contract MyToken is Initializable, ERC721Upgradeable, OwnableUpgradeable, UUPSUpgradeable { + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address initialOwner) initializer public { + __ERC721_init("MyToken", "MTK"); + __Ownable_init(initialOwner); + __UUPSUpgradeable_init(); + } + + function _authorizeUpgrade(address newImplementation) + internal + onlyOwner + override + {} + } + ` + } + }, { + 'myTokenV2.sol': { + content: ` + // SPDX-License-Identifier: MIT + pragma solidity ^0.8.20; + import "./myTokenV1.sol"; + + contract MyTokenV2 is MyToken { + function version () public view returns (string memory) { + return "MyTokenV2!"; + } + } + ` + } + }, { + 'initializeProxy.sol': { + content: ` + // SPDX-License-Identifier: MIT + pragma solidity ^0.8.20; + + import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; + import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + + contract MyInitializedToken is Initializable, ERC721Upgradeable, OwnableUpgradeable, UUPSUpgradeable { + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(string memory tokenName, string memory tokenSymbol, address initialOwner) initializer public { + __ERC721_init(tokenName, tokenSymbol); + __Ownable_init(initialOwner); + __UUPSUpgradeable_init(); + } + + function _authorizeUpgrade(address newImplementation) + internal + onlyOwner + override + {} + } + ` + } + } +] \ No newline at end of file diff --git a/apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts b/apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts index cebc84bc93..943292cc82 100644 --- a/apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts +++ b/apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts @@ -82,21 +82,21 @@ module.exports = { instanceAddress = address console.log('instanceAddress', instanceAddress) browser - .waitForElementVisible(`#instance${instanceAddress} [data-id="instanceContractBal"]`) + .waitForElementVisible(`#instance${instanceAddress} [data-id="instanceContractBal"]`) //*[@id="instance0xbBF289D846208c16EDc8474705C748aff07732dB" and contains(.,"Balance") and contains(.,'0.000000000000000111')] - .waitForElementVisible({ - locateStrategy: 'xpath', - selector: `//*[@id="instance${instanceAddress}" and contains(.,"Balance") and contains(.,'0.000000000000000111')]`, - timeout: 60000 - }) + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: `//*[@id="instance${instanceAddress}" and contains(.,"Balance") and contains(.,'0.000000000000000111')]`, + timeout: 60000 + }) //.waitForElementContainsText(`#instance${instanceAddress} [data-id="instanceContractBal"]`, 'Balance: 0.000000000000000111 ETH', 60000) - .clickFunction('sendSomeEther - transact (not payable)', { types: 'uint256 num', values: '2' }) - .pause(1000) - .waitForElementVisible({ - locateStrategy: 'xpath', - selector: `//*[@id="instance${instanceAddress}" and contains(.,"Balance") and contains(.,'0.000000000000000109')]`, - timeout: 60000 - }) + .clickFunction('sendSomeEther - transact (not payable)', { types: 'uint256 num', values: '2' }) + .pause(1000) + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: `//*[@id="instance${instanceAddress}" and contains(.,"Balance") and contains(.,'0.000000000000000109')]`, + timeout: 60000 + }) }) }, @@ -238,6 +238,95 @@ module.exports = { .executeScriptInTerminal('web3.eth.getAccounts()') .journalLastChildIncludes('[ "0x76a3ABb5a12dcd603B52Ed22195dED17ee82708f" ]') .end() + }, + + 'Should ensure that save environment state is checked by default #group4 #group5': function (browser: NightwatchBrowser) { + browser.waitForElementPresent('*[data-id="remixIdeSidePanel"]') + .clickLaunchIcon('settings') + .waitForElementPresent('[data-id="settingsEnableSaveEnvStateLabel"]') + .scrollInto('[data-id="settingsEnableSaveEnvStateLabel"]') + .verify.elementPresent('[data-id="settingsEnableSaveEnvState"]:checked') + }, + + 'Should deploy default storage contract; store value and ensure that state is saved. #group4 #group5': function (browser: NightwatchBrowser) { + browser + .clickLaunchIcon('filePanel') + .click('*[data-id="treeViewLitreeViewItemcontracts"]') + .openFile('contracts/1_Storage.sol') + .pause(5000) + .clickLaunchIcon('udapp') + .waitForElementPresent('*[data-id="Deploy - transact (not payable)"]') + .click('*[data-id="Deploy - transact (not payable)"]') + .waitForElementPresent('#instance0xd9145CCE52D386f254917e481eB44e9943F39138') + .clickInstance(0) + .clickFunction('store - transact (not payable)', { types: 'uint256 num', values: '10' }) + .clickFunction('retrieve - call') + .waitForElementContainsText('[data-id="treeViewLi0"]', 'uint256: 10') + .clickLaunchIcon('filePanel') + .openFile('.states/vm-shanghai/state.json') + .getEditorValue((content) => { + browser + .assert.ok(content.includes('"latestBlockNumber": "0x02"'), 'State is saved') + }) + }, + + 'Should load state after page refresh #group4': function (browser: NightwatchBrowser) { + browser.refreshPage() + .waitForElementVisible('*[data-id="remixIdeSidePanel"]') + .click('*[data-id="treeViewLitreeViewItemcontracts"]') + .openFile('contracts/1_Storage.sol') + .addAtAddressInstance('0xd9145CCE52D386f254917e481eB44e9943F39138', true, true, false) + .clickInstance(0) + .clickFunction('retrieve - call') + .waitForElementContainsText('[data-id="treeViewLi0"]', 'uint256: 10') + }, + + 'Should save state after running web3 script #group4': function (browser: NightwatchBrowser) { + browser + .clickLaunchIcon('settings') + .waitForElementPresent('[data-id="settingsTabGenerateContractMetadataLabel"]') + .click('[data-id="settingsTabGenerateContractMetadataLabel"]') + .verify.elementPresent('[data-id="settingsTabGenerateContractMetadata"]:checked') + .clickLaunchIcon('solidity') + .click('.remixui_compilerConfigSection') + .setValue('#evmVersionSelector', 'london') + .click('*[data-id="compilerContainerCompileBtn"]') + .pause(5000) + .clickLaunchIcon('udapp') + .switchEnvironment('vm-london') + .clickLaunchIcon('filePanel') + .click('*[data-id="treeViewLitreeViewItemscripts"]') + .openFile('scripts/deploy_with_web3.ts') + .click('[data-id="play-editor"]') + .waitForElementPresent('[data-id="treeViewDivDraggableItem.states/vm-london/state.json"]') + .click('[data-id="treeViewDivDraggableItem.states/vm-london/state.json"]') + .pause(100000) + .getEditorValue((content) => { + browser + .assert.ok(content.includes('"latestBlockNumber": "0x01"'), 'State is saved') + }) + }, + + 'Should ensure that .states is not updated when save env option is unchecked #group5': function (browser: NightwatchBrowser) { + browser + .clickLaunchIcon('settings') + .waitForElementPresent('[data-id="settingsEnableSaveEnvStateLabel"]') + .click('[data-id="settingsEnableSaveEnvStateLabel"]') + .verify.elementNotPresent('[data-id="settingsEnableSaveEnvState"]:checked') + .clickLaunchIcon('filePanel') + .openFile('contracts/1_Storage.sol') + .pause(5000) + .clickLaunchIcon('udapp') + .waitForElementPresent('*[data-id="Deploy - transact (not payable)"]') + .click('*[data-id="Deploy - transact (not payable)"]') + .pause(5000) + .clickLaunchIcon('filePanel') + .openFile('.states/vm-shanghai/state.json') + .getEditorValue((content) => { + browser + .assert.ok(content.includes('"latestBlockNumber": "0x02"'), 'State is unchanged') + }) + .end() } } diff --git a/apps/remix-ide/src/app/panels/tab-proxy.js b/apps/remix-ide/src/app/panels/tab-proxy.js index 984efe7534..fa6521023a 100644 --- a/apps/remix-ide/src/app/panels/tab-proxy.js +++ b/apps/remix-ide/src/app/panels/tab-proxy.js @@ -224,9 +224,23 @@ export class TabProxy extends Plugin { this.removeTab(oldName) } + /** + * + * @param {string} name + * @param {string} title + * @param {Function} switchTo + * @param {Function} close + * @param {string} icon + * @param {string} description + * @returns + */ addTab (name, title, switchTo, close, icon, description = '') { if (this._handlers[name]) return this.renderComponent() + if ((name.endsWith('.vy') && icon === undefined) || title.includes('Vyper')) { + icon = 'assets/img/vyperLogo2.webp' + } + var slash = name.split('/') const tabPath = slash.reverse() const tempTitle = [] @@ -292,7 +306,7 @@ export class TabProxy extends Plugin { if (!previous && tab.name === name) { if(index - 1 >= 0 && this.loadedTabs[index - 1]) previous = this.loadedTabs[index - 1] - else if (index + 1 && this.loadedTabs[index + 1]) + else if (index + 1 && this.loadedTabs[index + 1]) previous = this.loadedTabs[index + 1] } return tab.name !== name diff --git a/apps/remix-ide/src/app/tabs/locales/en/settings.json b/apps/remix-ide/src/app/tabs/locales/en/settings.json index aa8f67449c..2ea6de0d4b 100644 --- a/apps/remix-ide/src/app/tabs/locales/en/settings.json +++ b/apps/remix-ide/src/app/tabs/locales/en/settings.json @@ -43,5 +43,6 @@ "settings.copilot": "Solidity copilot - Alpha", "settings.copilot.activate": "Load & Activate copilot", "settings.copilot.max_new_tokens": "Maximum number of words to generate", - "settings.copilot.temperature": "Temperature" + "settings.copilot.temperature": "Temperature", + "settings.enableSaveEnvState": "Save environment state" } diff --git a/apps/remix-ide/src/app/tabs/locales/es/settings.json b/apps/remix-ide/src/app/tabs/locales/es/settings.json index 899e2787cc..5d7ddda6ab 100644 --- a/apps/remix-ide/src/app/tabs/locales/es/settings.json +++ b/apps/remix-ide/src/app/tabs/locales/es/settings.json @@ -36,5 +36,6 @@ "settings.port": "PUERTO", "settings.projectID": "ID DEL PROYECTO", "settings.projectSecret": "SECRETO DE PROYECTO", - "settings.analyticsInRemix": "Analíticas en IDE Remix" + "settings.analyticsInRemix": "Analíticas en IDE Remix", + "settings.enableSaveEnvState": "Save environment state" } diff --git a/apps/remix-ide/src/app/tabs/locales/fr/settings.json b/apps/remix-ide/src/app/tabs/locales/fr/settings.json index 5d1859dfe9..3b61ab68c2 100644 --- a/apps/remix-ide/src/app/tabs/locales/fr/settings.json +++ b/apps/remix-ide/src/app/tabs/locales/fr/settings.json @@ -36,5 +36,6 @@ "settings.port": "PORT", "settings.projectID": "ID du projet", "settings.projectSecret": "SECRET DU PROJET", - "settings.analyticsInRemix": "Analytics dans l'IDE de Remix" + "settings.analyticsInRemix": "Analytics dans l'IDE de Remix", + "settings.enableSaveEnvState": "Save environment state" } diff --git a/apps/remix-ide/src/app/tabs/locales/it/settings.json b/apps/remix-ide/src/app/tabs/locales/it/settings.json index 416f338b64..251089f6cb 100644 --- a/apps/remix-ide/src/app/tabs/locales/it/settings.json +++ b/apps/remix-ide/src/app/tabs/locales/it/settings.json @@ -36,5 +36,6 @@ "settings.port": "PORTA", "settings.projectID": "ID PROGETTO", "settings.projectSecret": "SEGRETO DEL PROGETTO", - "settings.analyticsInRemix": "Analytics nella Remix IDE" + "settings.analyticsInRemix": "Analytics nella Remix IDE", + "settings.enableSaveEnvState": "Save environment state" } diff --git a/apps/remix-ide/src/app/tabs/locales/zh/settings.json b/apps/remix-ide/src/app/tabs/locales/zh/settings.json index 5504874efd..8a27f4a83d 100644 --- a/apps/remix-ide/src/app/tabs/locales/zh/settings.json +++ b/apps/remix-ide/src/app/tabs/locales/zh/settings.json @@ -36,5 +36,6 @@ "settings.port": "端口", "settings.projectID": "项目 ID", "settings.projectSecret": "项目密钥", - "settings.analyticsInRemix": "Remix IDE 中的分析功能" + "settings.analyticsInRemix": "Remix IDE 中的分析功能", + "settings.enableSaveEnvState": "Save environment state" } diff --git a/apps/remix-ide/src/app/tabs/web3-provider.js b/apps/remix-ide/src/app/tabs/web3-provider.js index 16c3a3dd17..d4efb04a77 100644 --- a/apps/remix-ide/src/app/tabs/web3-provider.js +++ b/apps/remix-ide/src/app/tabs/web3-provider.js @@ -63,6 +63,13 @@ export class Web3ProviderModule extends Plugin { await this.call('compilerArtefacts', 'addResolvedContract', contractAddressStr, data) } }, 50) + const isVM = this.blockchain.executionContext.isVM() + + if (isVM && this.blockchain.config.get('settings/save-evm-state')) { + await this.blockchain.executionContext.getStateDetails().then((state) => { + this.call('fileManager', 'writeFile', `.states/${this.blockchain.executionContext.getProvider()}/state.json`, state) + }) + } } } resolve(message) diff --git a/apps/remix-ide/src/assets/img/vyperLogo2.webp b/apps/remix-ide/src/assets/img/vyperLogo2.webp new file mode 100644 index 0000000000..fc85993627 Binary files /dev/null and b/apps/remix-ide/src/assets/img/vyperLogo2.webp differ diff --git a/apps/remix-ide/src/blockchain/blockchain.tsx b/apps/remix-ide/src/blockchain/blockchain.tsx index fe9b6598f1..8095777a00 100644 --- a/apps/remix-ide/src/blockchain/blockchain.tsx +++ b/apps/remix-ide/src/blockchain/blockchain.tsx @@ -135,7 +135,8 @@ export class Blockchain extends Plugin { setupEvents() { this.executionContext.event.register('contextChanged', async (context) => { - await this.resetEnvironment() + // reset environment to last known state of the context + await this.loadContext(context) this._triggerEvent('contextChanged', [context]) this.detectNetwork((error, network) => { this.networkStatus = {network, error} @@ -643,8 +644,23 @@ export class Blockchain extends Plugin { }) } - async resetEnvironment() { - await this.getCurrentProvider().resetEnvironment() + async loadContext(context: string) { + const saveEvmState = this.config.get('settings/save-evm-state') + + if (saveEvmState) { + const contextExists = await this.call('fileManager', 'exists', `.states/${context}/state.json`) + + if (contextExists) { + const stateDb = await this.call('fileManager', 'readFile', `.states/${context}/state.json`) + + await this.getCurrentProvider().resetEnvironment(stateDb) + } else { + await this.getCurrentProvider().resetEnvironment() + } + } else { + await this.getCurrentProvider().resetEnvironment() + } + // TODO: most params here can be refactored away in txRunner const web3Runner = new TxRunnerWeb3( { @@ -677,7 +693,7 @@ export class Blockchain extends Plugin { view on etherscan ) - } + } }) }) this.txRunner = new TxRunner(web3Runner, {}) @@ -889,8 +905,13 @@ export class Blockchain extends Plugin { let execResult let returnValue = null if (isVM) { - const hhlogs = await this.web3().remix.getHHLogsForTx(txResult.transactionHash) + if (!tx.useCall && this.config.get('settings/save-evm-state')) { + await this.executionContext.getStateDetails().then((state) => { + this.call('fileManager', 'writeFile', `.states/${this.executionContext.getProvider()}/state.json`, state) + }) + } + const hhlogs = await this.web3().remix.getHHLogsForTx(txResult.transactionHash) if (hhlogs && hhlogs.length) { const finalLogs = (
diff --git a/apps/remix-ide/src/blockchain/execution-context.js b/apps/remix-ide/src/blockchain/execution-context.js index 7c33bb4f27..89ad383c7c 100644 --- a/apps/remix-ide/src/blockchain/execution-context.js +++ b/apps/remix-ide/src/blockchain/execution-context.js @@ -3,6 +3,7 @@ import Web3 from 'web3' import { execution } from '@remix-project/remix-lib' import EventManager from '../lib/events' +import {bufferToHex} from '@ethereumjs/util' const _paq = window._paq = window._paq || [] let web3 @@ -71,35 +72,44 @@ export class ExecutionContext { } detectNetwork (callback) { - if (this.isVM()) { - callback(null, { id: '-', name: 'VM' }) - } else { - if (!web3.currentProvider) { - return callback('No provider set') - } - const cb = (err, id) => { - let name = null - if (err) name = 'Unknown' - // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md - else if (id === 1) name = 'Main' - else if (id === 3) name = 'Ropsten' - else if (id === 4) name = 'Rinkeby' - else if (id === 5) name = 'Goerli' - else if (id === 42) name = 'Kovan' - else if (id === 11155111) name = 'Sepolia' - else name = 'Custom' - - if (id === 1) { - web3.eth.getBlock(0).then((block) => { - if (block && block.hash !== this.mainNetGenesisHash) name = 'Custom' - callback(err, { id, name, lastBlock: this.lastBlock, currentFork: this.currentFork }) - }).catch((error) => callback(error)) - } else { - callback(err, { id, name, lastBlock: this.lastBlock, currentFork: this.currentFork }) + return new Promise((resolve, reject) => { + if (this.isVM()) { + callback && callback(null, { id: '-', name: 'VM' }) + return resolve({ id: '-', name: 'VM' }) + } else { + if (!web3.currentProvider) { + callback && callback('No provider set') + return reject('No provider set') + } + const cb = (err, id) => { + let name = null + if (err) name = 'Unknown' + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md + else if (id === 1) name = 'Main' + else if (id === 3) name = 'Ropsten' + else if (id === 4) name = 'Rinkeby' + else if (id === 5) name = 'Goerli' + else if (id === 42) name = 'Kovan' + else if (id === 11155111) name = 'Sepolia' + else name = 'Custom' + + if (id === 1) { + web3.eth.getBlock(0).then((block) => { + if (block && block.hash !== this.mainNetGenesisHash) name = 'Custom' + callback && callback(err, { id, name, lastBlock: this.lastBlock, currentFork: this.currentFork }) + return resolve({ id, name, lastBlock: this.lastBlock, currentFork: this.currentFork }) + }).catch((error) => { + callback && callback(error) + return reject(error) + }) + } else { + callback && callback(err, { id, name, lastBlock: this.lastBlock, currentFork: this.currentFork }) + return resolve({ id, name, lastBlock: this.lastBlock, currentFork: this.currentFork }) + } } + web3.eth.net.getId().then(id=>cb(null,parseInt(id))).catch(err=>cb(err)) } - web3.eth.net.getId().then(id=>cb(null,parseInt(id))).catch(err=>cb(err)) - } + }) } removeProvider (name) { @@ -195,4 +205,26 @@ export class ExecutionContext { return transactionDetailsLinks[network] + hash } } + + async getStateDetails() { + const db = await this.web3().remix.getStateDb() + const blocksData = await this.web3().remix.getBlocksData() + const state = { + db: Object.fromEntries(db._database), + blocks: blocksData.blocks, + latestBlockNumber: blocksData.latestBlockNumber + } + const stringifyed = JSON.stringify(state, (key, value) => { + if (key === 'db') { + return value + } else if (key === 'blocks') { + return value.map(block => bufferToHex(block)) + }else if (key === '') { + return value + } + return bufferToHex(value) + }, '\t') + + return stringifyed + } } diff --git a/apps/remix-ide/src/blockchain/providers/vm.ts b/apps/remix-ide/src/blockchain/providers/vm.ts index 67357b347e..5156206c1c 100644 --- a/apps/remix-ide/src/blockchain/providers/vm.ts +++ b/apps/remix-ide/src/blockchain/providers/vm.ts @@ -2,6 +2,7 @@ import Web3, { FMT_BYTES, FMT_NUMBER, LegacySendAsyncProvider } from 'web3' import { fromWei, toBigInt } from 'web3-utils' import { privateToAddress, hashPersonalMessage, isHexString } from '@ethereumjs/util' import { extend, JSONRPCRequestPayload, JSONRPCResponseCallback } from '@remix-project/remix-simulator' +import {toBuffer} from '@ethereumjs/util' import { ExecutionContext } from '../execution-context' export class VMProvider { @@ -12,9 +13,7 @@ export class VMProvider { sendAsync: (query: JSONRPCRequestPayload, callback: JSONRPCResponseCallback) => void } newAccountCallback: {[stamp: number]: (error: Error, address: string) => void} - constructor (executionContext: ExecutionContext) { - this.executionContext = executionContext this.worker = null this.provider = null @@ -29,7 +28,7 @@ export class VMProvider { }) } - async resetEnvironment () { + async resetEnvironment (stringifiedState?: string) { if (this.worker) this.worker.terminate() this.worker = new Worker(new URL('./worker-vm', import.meta.url)) const provider = this.executionContext.getProviderObject() @@ -76,10 +75,35 @@ export class VMProvider { } } }) - this.worker.postMessage({ cmd: 'init', fork: this.executionContext.getCurrentFork(), nodeUrl: provider?.options['nodeUrl'], blockNumber: provider?.options['blockNumber']}) + if (stringifiedState) { + try { + const blockchainState = JSON.parse(stringifiedState) + const blockNumber = parseInt(blockchainState.latestBlockNumber, 16) + const stateDb = blockchainState.db + + this.worker.postMessage({ + cmd: 'init', + fork: this.executionContext.getCurrentFork(), + nodeUrl: provider?.options['nodeUrl'], + blockNumber, + stateDb, + blocks: blockchainState.blocks + }) + } catch (e) { + console.error(e) + } + } else { + this.worker.postMessage({ + cmd: 'init', + fork: this.executionContext.getCurrentFork(), + nodeUrl: provider?.options['nodeUrl'], + blockNumber: provider?.options['blockNumber'] + }) + } }) } + // TODO: is still here because of the plugin API // can be removed later when we update the API createVMAccount (newAccount) { diff --git a/apps/remix-ide/src/blockchain/providers/worker-vm.ts b/apps/remix-ide/src/blockchain/providers/worker-vm.ts index 64a8d0255b..e91e30dfd4 100644 --- a/apps/remix-ide/src/blockchain/providers/worker-vm.ts +++ b/apps/remix-ide/src/blockchain/providers/worker-vm.ts @@ -6,7 +6,7 @@ self.onmessage = (e: MessageEvent) => { switch (data.cmd) { case 'init': { - provider = new Provider({ fork: data.fork, nodeUrl: data.nodeUrl, blockNumber: data.blockNumber }) + provider = new Provider({ fork: data.fork, nodeUrl: data.nodeUrl, blockNumber: data.blockNumber, stateDb: data.stateDb, blocks: data.blocks}) provider.init().then(() => { self.postMessage({ cmd: 'initiateResult', diff --git a/apps/vyper/src/app/app.css b/apps/vyper/src/app/app.css index 23eae179b3..2d05f1fead 100644 --- a/apps/vyper/src/app/app.css +++ b/apps/vyper/src/app/app.css @@ -262,3 +262,7 @@ html, body, #root, main { padding: 0; border-radius: 0; } + +.vyper-panel-width { + width: 94%; +} diff --git a/apps/vyper/src/app/app.tsx b/apps/vyper/src/app/app.tsx index 18fc0f5a86..ed16a3f6d0 100644 --- a/apps/vyper/src/app/app.tsx +++ b/apps/vyper/src/app/app.tsx @@ -131,19 +131,25 @@ const App = () => {
-
+
-
- setEnvironment('remote')} label="Remote Compiler" className={`${state.environment === 'remote' ? 'd-flex mr-4' : 'd-flex mr-4 cursor-status'}`} /> - setEnvironment('local')} label="Local Compiler" className={`${state.environment === 'local' ? '' : `cursor-status`}`} /> +
+
+ setEnvironment('remote')} className={`custom-control-input ${state.environment === 'remote' ? 'd-flex mr-1' : 'd-flex mr-1 cursor-status'}`} /> + +
+
+ setEnvironment('local')} className={`custom-control-input ${state.environment === 'local' ? '' : `cursor-status`}`} /> + +
+
- - + Specify the{' '} compiler version @@ -164,7 +170,7 @@ const App = () => { ) : output.status === 'failed' ? ( - + ) : null} diff --git a/apps/vyper/src/app/components/CompileErrorCard.tsx b/apps/vyper/src/app/components/CompileErrorCard.tsx index 761d64f6e8..dce0afe26b 100644 --- a/apps/vyper/src/app/components/CompileErrorCard.tsx +++ b/apps/vyper/src/app/components/CompileErrorCard.tsx @@ -1,15 +1,13 @@ import {CopyToClipboard} from '@remix-ui/clipboard' import Reaact from 'react' +import { RemixClient } from '../utils' -export function CompileErrorCard(props: any) { +export function CompileErrorCard(props: { output: any, plugin: RemixClient }) { return (
{props.output.message.trim()} - {/*
+
- props.askGpt(props.output.message)}> + await props.plugin.askGpt(props.output.message)}> Ask GPT
-
*/} +
) } diff --git a/apps/vyper/src/app/components/VyperResult.tsx b/apps/vyper/src/app/components/VyperResult.tsx index a1f799cfb0..0efc4afc28 100644 --- a/apps/vyper/src/app/components/VyperResult.tsx +++ b/apps/vyper/src/app/components/VyperResult.tsx @@ -58,9 +58,8 @@ function VyperResult({ output, plugin }: VyperResultProps) { return ( <> -
-
-
) diff --git a/libs/remix-ui/settings/src/lib/settingsAction.ts b/libs/remix-ui/settings/src/lib/settingsAction.ts index 6e59d25ffb..22110f8cb9 100644 --- a/libs/remix-ui/settings/src/lib/settingsAction.ts +++ b/libs/remix-ui/settings/src/lib/settingsAction.ts @@ -90,3 +90,8 @@ export const saveIpfsSettingsToast = (config, dispatch, ipfsURL, ipfsProtocol, i config.set('settings/ipfs-project-secret', ipfsProjectSecret) dispatch({ type: 'save', payload: { message: 'IPFS settings have been saved' } }) } + +export const saveEnvState = (config, checked, dispatch) => { + config.set('settings/save-evm-state', checked) + dispatch({ type: 'save-evm-state', payload: { isChecked: checked, textClass: checked ? textDark : textSecondary } }) +} diff --git a/libs/remix-ui/settings/src/lib/settingsReducer.ts b/libs/remix-ui/settings/src/lib/settingsReducer.ts index abad510431..cfd8572f9e 100644 --- a/libs/remix-ui/settings/src/lib/settingsReducer.ts +++ b/libs/remix-ui/settings/src/lib/settingsReducer.ts @@ -56,7 +56,12 @@ export const initialState = { name: 'copilot/suggest/temperature', value: 0.5, textClass: textSecondary - } + }, + { + name: 'save-evm-state', + isChecked: false, + textClass: textSecondary + }, ] } @@ -173,6 +178,17 @@ export const settingReducer = (state, action) => { return { ...state } + + case 'save-evm-state': + state.elementState.map(element => { + if (element.name === 'save-evm-state') { + element.isChecked = action.payload.isChecked + element.textClass = action.payload.textClass + } + }) + return { + ...state + } default: return initialState } diff --git a/libs/remix-ui/solidity-compiler/src/lib/api/compiler-api.ts b/libs/remix-ui/solidity-compiler/src/lib/api/compiler-api.ts index 68e62915c5..0528aeda0a 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/api/compiler-api.ts +++ b/libs/remix-ui/solidity-compiler/src/lib/api/compiler-api.ts @@ -28,7 +28,7 @@ export const CompilerApiMixin = (Base) => class extends Base { onContentChanged: () => void onFileClosed: (name: string) => void statusChanged: (data: { key: string, title?: string, type?: string }) => void - + setSolJsonBinData: (urls: iSolJsonBinData) => void initCompilerApi () { @@ -336,7 +336,7 @@ export const CompilerApiMixin = (Base) => class extends Base { await this.call('editor', 'addAnnotation', pos, file) } } - } + } } this.compiler.event.register('compilationFinished', this.data.eventHandlers.onCompilationFinished) @@ -360,6 +360,8 @@ export const CompilerApiMixin = (Base) => class extends Base { else this.compileTabLogic.runCompiler(undefined) } else if (this.currentFile && this.currentFile.endsWith('.circom')) { await this.call('circuit-compiler', 'compile', this.currentFile) + } else if (this.currentFile && this.currentFile.endsWith('.vy')) { + await this.call('vyper', 'vyperCompileCustomAction', this.currentFile) } } } @@ -370,7 +372,7 @@ export const CompilerApiMixin = (Base) => class extends Base { return new Promise((resolve) => { if (!data.contracts || (data.contracts && Object.keys(data.contracts).length === 0)) { return resolve({ - contractMap: {}, + contractMap: {}, contractsDetails: {}, target: source.target }) @@ -386,7 +388,7 @@ export const CompilerApiMixin = (Base) => class extends Base { ) }) return resolve({ - contractMap, + contractMap, contractsDetails, target: source.target })