diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index d0b804da2a..0000000000 --- a/.prettierignore +++ /dev/null @@ -1,4 +0,0 @@ -# Add files here to ignore them from prettier formatting - -/dist -/coverage diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 544138be45..0000000000 --- a/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "singleQuote": true -} diff --git a/README.md b/README.md index aea4299352..06fd3fbe16 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,24 @@ -[![CircleCI](https://circleci.com/gh/ethereum/remix-project.svg?style=svg)](https://circleci.com/gh/ethereum/remix-project) -[![Documentation Status](https://readthedocs.org/projects/docs/badge/?version=latest)](https://remix-ide.readthedocs.io/en/latest/index.html) -[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/ethereum/remix-project/blob/master/CONTRIBUTING.md) -![GitHub contributors](https://img.shields.io/github/contributors/ethereum/remix-project) -[![Awesome Remix](https://img.shields.io/badge/Awesome--Remix-resources-green)](https://github.com/ethereum/awesome-remix) +

+ Remix Logo +

+

Remix Project

+ +
+ + +[![CircleCI](https://img.shields.io/circleci/build/github/ethereum/remix-project?logo=circleci)](https://circleci.com/gh/ethereum/remix-project) +[![Documentation Status](https://readthedocs.org/projects/remix-ide/badge/?version=latest)](https://remix-ide.readthedocs.io/en/latest/index.html) +[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat&logo=github)](https://github.com/ethereum/remix-project/blob/master/CONTRIBUTING.md) +[![GitHub contributors](https://img.shields.io/github/contributors/ethereum/remix-project?style=flat&logo=github)](https://github.com/ethereum/remix-project/blob/master/CONTRIBUTING.md) +[![Awesome Remix](https://img.shields.io/badge/Awesome--Remix-resources-green?logo=awesomelists)](https://github.com/ethereum/awesome-remix) ![GitHub](https://img.shields.io/github/license/ethereum/remix-project) -[![Join the chat at https://gitter.im/ethereum/remix](https://badges.gitter.im/ethereum/remix.svg)](https://gitter.im/ethereum/remix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Twitter Follow](https://img.shields.io/twitter/follow/ethereumremix?style=social)](https://twitter.com/ethereumremix) +[![Gitter Chat](https://img.shields.io/badge/Gitter%20-chat-brightgreen?style=plastic&logo=gitter)](https://gitter.im/ethereum/remix) +[![Twitter Follow](https://img.shields.io/twitter/follow/ethereumremix?style=flat&logo=twitter&color=green)](https://twitter.com/ethereumremix) + +
+ +## Remix Project -# Remix Project **Remix Project** is a rich toolset including Remix IDE, a comprehensive smart contract development tool. The Remix Project also includes Remix Plugin Engine and Remix Libraries which are low-level tools for wider use. ## Remix IDE diff --git a/apps/remix-ide-e2e/src/commands/currentWorkspaceIs.ts b/apps/remix-ide-e2e/src/commands/currentWorkspaceIs.ts index a090735f45..26da9cf9c8 100644 --- a/apps/remix-ide-e2e/src/commands/currentWorkspaceIs.ts +++ b/apps/remix-ide-e2e/src/commands/currentWorkspaceIs.ts @@ -3,15 +3,15 @@ import EventEmitter from 'events' class CurrentWorkspaceIs extends EventEmitter { command (this: NightwatchBrowser, name: string): NightwatchBrowser { - this.api - .execute(function () { - const el = document.querySelector('select[data-id="workspacesSelect"]') as HTMLSelectElement - return el.value - }, [], (result) => { - console.log(result) - this.api.assert.equal(result.value, name) - this.emit('complete') - }) + const browser = this.api + + browser.getText('[data-id="workspacesSelect"]', function (result) { + browser.assert.equal(result.value, name) + }) + .perform((done) => { + done() + this.emit('complete') + }) return this } } diff --git a/apps/remix-ide-e2e/src/commands/switchEnvironment.ts b/apps/remix-ide-e2e/src/commands/switchEnvironment.ts new file mode 100644 index 0000000000..03564ced41 --- /dev/null +++ b/apps/remix-ide-e2e/src/commands/switchEnvironment.ts @@ -0,0 +1,18 @@ +import { NightwatchBrowser } from 'nightwatch' +import EventEmitter from 'events' + +class switchEnvironment extends EventEmitter { + command (this: NightwatchBrowser, provider: string): NightwatchBrowser { + this.api.waitForElementVisible('[data-id="settingsSelectEnvOptions"]') + .click('[data-id="settingsSelectEnvOptions"] button') + .waitForElementVisible(`[data-id="dropdown-item-${provider}"]`) + .click(`[data-id="dropdown-item-${provider}"]`) + .perform((done) => { + done() + this.emit('complete') + }) + return this + } +} + +module.exports = switchEnvironment diff --git a/apps/remix-ide-e2e/src/commands/switchWorkspace.ts b/apps/remix-ide-e2e/src/commands/switchWorkspace.ts new file mode 100644 index 0000000000..6219ddd218 --- /dev/null +++ b/apps/remix-ide-e2e/src/commands/switchWorkspace.ts @@ -0,0 +1,18 @@ +import { NightwatchBrowser } from 'nightwatch' +import EventEmitter from 'events' + +class switchWorkspace extends EventEmitter { + command (this: NightwatchBrowser, workspaceName: string): NightwatchBrowser { + this.api.waitForElementVisible('[data-id="workspacesSelect"]') + .click('[data-id="workspacesSelect"]') + .waitForElementVisible(`[data-id="dropdown-item-${workspaceName}"]`) + .click(`[data-id="dropdown-item-${workspaceName}"]`) + .perform((done) => { + done() + this.emit('complete') + }) + return this + } +} + +module.exports = switchWorkspace diff --git a/apps/remix-ide-e2e/src/tests/ballot.test.ts b/apps/remix-ide-e2e/src/tests/ballot.test.ts index 5244241e89..1c8383f047 100644 --- a/apps/remix-ide-e2e/src/tests/ballot.test.ts +++ b/apps/remix-ide-e2e/src/tests/ballot.test.ts @@ -83,21 +83,15 @@ module.exports = { browser .openFile('Untitled.sol') .clickLaunchIcon('udapp') - .click('*[data-id="settingsWeb3Mode"]') - .waitForElementPresent('[data-id="envNotification-modal-footer-ok-react"]') + .switchEnvironment('External Http Provider') + .waitForElementPresent('[data-id="basic-http-provider-modal-footer-ok-react"]') .execute(function () { - const modal = document.querySelector('[data-id="envNotification-modal-footer-ok-react"]') as any + const modal = document.querySelector('[data-id="basic-http-provider-modal-footer-ok-react"]') as any modal.click() }) .pause(5000) - .execute(function () { - const env: any = document.getElementById('selectExEnvOptions') - - return env.value - }, [], function (result) { - browser.assert.ok(result.value === 'web3', 'Web3 Provider not selected') - }) + .waitForElementContainsText('#selectExEnvOptions button', 'External Http Provider') .clickLaunchIcon('solidity') .clickLaunchIcon('udapp') .pause(2000) diff --git a/apps/remix-ide-e2e/src/tests/ballot_0_4_11.test.ts b/apps/remix-ide-e2e/src/tests/ballot_0_4_11.test.ts index 28b03219fc..9a36ad67f4 100644 --- a/apps/remix-ide-e2e/src/tests/ballot_0_4_11.test.ts +++ b/apps/remix-ide-e2e/src/tests/ballot_0_4_11.test.ts @@ -78,10 +78,10 @@ module.exports = { browser .openFile('Untitled.sol') .clickLaunchIcon('udapp') - .click('*[data-id="settingsWeb3Mode"]') - .waitForElementPresent('[data-id="envNotification-modal-footer-ok-react"]') + .switchEnvironment('External Http Provider') + .waitForElementPresent('[data-id="basic-http-provider-modal-footer-ok-react"]') .execute(function () { - const modal = document.querySelector('[data-id="envNotification-modal-footer-ok-react"]') as any + const modal = document.querySelector('[data-id="basic-http-provider-modal-footer-ok-react"]') as any modal.click() }) diff --git a/apps/remix-ide-e2e/src/tests/debugger.test.ts b/apps/remix-ide-e2e/src/tests/debugger.test.ts index d46c1020ce..19aa196b60 100644 --- a/apps/remix-ide-e2e/src/tests/debugger.test.ts +++ b/apps/remix-ide-e2e/src/tests/debugger.test.ts @@ -214,7 +214,7 @@ module.exports = { .setSolidityCompilerVersion('soljson-v0.8.7+commit.e28d00a7.js') .addFile('useDebugNodes.sol', sources[5]['useDebugNodes.sol']) // compile contract .clickLaunchIcon('udapp') - .click('*[data-id="settingsWeb3Mode"]') // select web3 provider with debug nodes URL + .switchEnvironment('External Http Provider') // select web3 provider with debug nodes URL .clearValue('*[data-id="modalDialogCustomPromptText"]') .setValue('*[data-id="modalDialogCustomPromptText"]', 'https://remix-rinkeby.ethdevops.io') .modalFooterOKClick() diff --git a/apps/remix-ide-e2e/src/tests/generalSettings.test.ts b/apps/remix-ide-e2e/src/tests/generalSettings.test.ts index ca54678a7b..a4932b05fd 100644 --- a/apps/remix-ide-e2e/src/tests/generalSettings.test.ts +++ b/apps/remix-ide-e2e/src/tests/generalSettings.test.ts @@ -41,7 +41,7 @@ module.exports = { .setValue('*[data-id="settingsTabGistAccessToken"]', '**********') .click('*[data-id="settingsTabSaveGistToken"]') .waitForElementVisible('*[data-shared="tooltipPopup"]', 5000) - .assert.containsText('*[data-shared="tooltipPopup"]', 'Access token has been saved') + .assert.containsText('*[data-shared="tooltipPopup"]', 'GitHub credentials updated') .pause(3000) }, @@ -59,7 +59,7 @@ module.exports = { .pause(1000) .click('*[data-id="settingsTabRemoveGistToken"]') .waitForElementVisible('*[data-shared="tooltipPopup"]', 5000) - .assert.containsText('*[data-shared="tooltipPopup"]', 'Access token removed') + .assert.containsText('*[data-shared="tooltipPopup"]', 'GitHub credentials removed') .assert.containsText('*[data-id="settingsTabGistAccessToken"]', '') }, diff --git a/apps/remix-ide-e2e/src/tests/migrateFileSystem.test.ts b/apps/remix-ide-e2e/src/tests/migrateFileSystem.test.ts index 3d04a3edfc..0914f6c2f5 100644 --- a/apps/remix-ide-e2e/src/tests/migrateFileSystem.test.ts +++ b/apps/remix-ide-e2e/src/tests/migrateFileSystem.test.ts @@ -81,7 +81,7 @@ module.exports = { // these are test data entries 'Should have a workspace_test #group1 #group3 #group5 #group7': function (browser: NightwatchBrowser) { browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000) - .click('*[data-id="workspacesSelect"] option[value="workspace_test"]') + .switchWorkspace('workspace_test') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtest_contracts"]') }, 'Should have a sol file with test data #group1 #group3 #group5 #group7': function (browser: NightwatchBrowser) { @@ -103,7 +103,7 @@ module.exports = { }, 'Should have a empty workspace #group1 #group3 #group5 #group7': function (browser: NightwatchBrowser) { browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000) - .click('*[data-id="workspacesSelect"] option[value="emptyspace"]') + .switchWorkspace('emptyspace') }, // end of test data entries 'Should load with all storage blocked #group4': function (browser: NightwatchBrowser) { diff --git a/apps/remix-ide-e2e/src/tests/plugin_api.ts b/apps/remix-ide-e2e/src/tests/plugin_api.ts index faabf03247..c7ef41a231 100644 --- a/apps/remix-ide-e2e/src/tests/plugin_api.ts +++ b/apps/remix-ide-e2e/src/tests/plugin_api.ts @@ -188,7 +188,7 @@ module.exports = { .frameParent() .useCss() .clickLaunchIcon('udapp') - .waitForElementContainsText('#selectExEnvOptions option:checked', 'JavaScript VM (Berlin)') + .waitForElementContainsText('#selectExEnvOptions button', 'Remix VM (Berlin)') .clickLaunchIcon('localPlugin') .useXpath() // @ts-ignore @@ -298,25 +298,25 @@ module.exports = { }, null, null) }, 'Should get all workspaces #group2': async function (browser: NightwatchBrowser) { - await clickAndCheckLog(browser, 'filePanel:getWorkspaces', ['default_workspace', 'emptyworkspace', 'testspace'], null, null) + await clickAndCheckLog(browser, 'filePanel:getWorkspaces', [{name:"default_workspace",isGitRepo:false}, {name:"emptyworkspace",isGitRepo:false}, {name:"testspace",isGitRepo:false}], null, null) }, 'Should have set workspace event #group2': async function (browser: NightwatchBrowser) { await clickAndCheckLog(browser, 'filePanel:createWorkspace', null, { event: 'setWorkspace', args: [{ name: 'newspace', isLocalhost: false }] }, 'newspace') }, 'Should have event when switching workspace #group2': async function (browser: NightwatchBrowser) { // @ts-ignore - browser.frameParent().useCss().clickLaunchIcon('filePanel').click('*[data-id="workspacesSelect"] option[value="default_workspace"]').useXpath().click('//*[@data-id="verticalIconsKindlocalPlugin"]').frame(0, async () => { + browser.frameParent().useCss().clickLaunchIcon('filePanel').switchWorkspace('default_workspace').useXpath().click('//*[@data-id="verticalIconsKindlocalPlugin"]').frame(0, async () => { await clickAndCheckLog(browser, null, null, { event: 'setWorkspace', args: [{ name: 'default_workspace', isLocalhost: false }] }, null) }) }, 'Should rename workspace #group2': async function (browser: NightwatchBrowser) { await clickAndCheckLog(browser, 'filePanel:renameWorkspace', null, null, ['default_workspace', 'renamed']) - await clickAndCheckLog(browser, 'filePanel:getWorkspaces', ['emptyworkspace', 'testspace', 'newspace', 'renamed'], null, null) + await clickAndCheckLog(browser, 'filePanel:getWorkspaces', [{name:"emptyworkspace",isGitRepo:false},{name:"testspace",isGitRepo:false},{name:"newspace",isGitRepo:false},{name:"renamed",isGitRepo:false}], null, null) }, 'Should delete workspace #group2': async function (browser: NightwatchBrowser) { await clickAndCheckLog(browser, 'filePanel:deleteWorkspace', null, null, ['testspace']) - await clickAndCheckLog(browser, 'filePanel:getWorkspaces', ['emptyworkspace', 'newspace', 'renamed'], null, null) + await clickAndCheckLog(browser, 'filePanel:getWorkspaces', [{name:"emptyworkspace",isGitRepo:false},{name:"newspace",isGitRepo:false},{name:"renamed",isGitRepo:false}], null, null) }, // DGIT 'Should have changes on new workspace #group3': async function (browser: NightwatchBrowser) { @@ -391,7 +391,7 @@ module.exports = { .useCss() .clickLaunchIcon('pluginManager') .clickLaunchIcon('udapp') - .click('*[data-id="Hardhat Provider"]') + .switchEnvironment('Hardhat Provider') .modalFooterOKClick('hardhat-provider') .waitForElementContainsText('*[data-id="settingsNetworkEnv"]', 'Custom') // e.g Custom (1337) network .clickLaunchIcon('localPlugin') diff --git a/apps/remix-ide-e2e/src/tests/providers.test.ts b/apps/remix-ide-e2e/src/tests/providers.test.ts index b2c883fd53..5cb1048d91 100644 --- a/apps/remix-ide-e2e/src/tests/providers.test.ts +++ b/apps/remix-ide-e2e/src/tests/providers.test.ts @@ -10,7 +10,7 @@ module.exports = { 'Should switch to ganache provider, set a custom URL and fail to connect': function (browser: NightwatchBrowser) { browser.waitForElementVisible('div[data-id="remixIdeIconPanel"]', 10000) .clickLaunchIcon('udapp') - .click('*[data-id="Ganache Provider"]') + .switchEnvironment('Ganache Provider') .waitForElementVisible('*[data-id="ganache-providerModalDialogModalBody-react"]') .execute(() => { (document.querySelector('*[data-id="ganache-providerModalDialogModalBody-react"] input') as any).focus() @@ -25,7 +25,7 @@ module.exports = { }, 'Should switch to ganache provider, use the default ganache URL and succeed to connect': function (browser: NightwatchBrowser) { - browser.click('*[data-id="Ganache Provider"]') + browser.switchEnvironment('Ganache Provider') .waitForElementVisible('*[data-id="ganache-providerModalDialogModalBody-react"]') .modalFooterOKClick('ganache-provider') .waitForElementContainsText('*[data-id="settingsNetworkEnv"]', 'Custom (') @@ -33,7 +33,7 @@ module.exports = { 'Should switch to foundry provider, set a custom URL and fail to connect': function (browser: NightwatchBrowser) { browser.waitForElementVisible('div[data-id="remixIdeIconPanel"]', 10000) - .click('*[data-id="Foundry Provider"]') + .switchEnvironment('Foundry Provider') .waitForElementVisible('*[data-id="foundry-providerModalDialogModalBody-react"]') .execute(() => { (document.querySelector('*[data-id="foundry-providerModalDialogModalBody-react"] input') as any).focus() @@ -44,14 +44,11 @@ module.exports = { .waitForElementContainsText('*[data-id="foundry-providerModalDialogModalBody-react"]', 'Error while connecting to the provider') .modalFooterOKClick('foundry-provider') .waitForElementNotVisible('*[data-id="foundry-providerModalDialogModalBody-react"]') - .waitForElementVisible('*[data-id="PermissionHandler-modal-footer-ok-react"]') - .click('*[data-id="PermissionHandler-modal-footer-ok-react"]') - .waitForElementNotVisible('*[data-id="PermissionHandler-modal-footer-ok-react"]') .pause(1000) }, 'Should switch to foundry provider, use the default foundry URL and succeed to connect': function (browser: NightwatchBrowser) { - browser.click('*[data-id="Foundry Provider"]') + browser.switchEnvironment('Foundry Provider') .waitForElementVisible('*[data-id="foundry-providerModalDialogModalBody-react"]') .modalFooterOKClick('foundry-provider') .waitForElementContainsText('*[data-id="settingsNetworkEnv"]', 'Custom (') diff --git a/apps/remix-ide-e2e/src/tests/recorder.test.ts b/apps/remix-ide-e2e/src/tests/recorder.test.ts index 1b41d856da..7e8748b737 100644 --- a/apps/remix-ide-e2e/src/tests/recorder.test.ts +++ b/apps/remix-ide-e2e/src/tests/recorder.test.ts @@ -42,7 +42,7 @@ module.exports = { .createContract(['12']) .clickInstance(0) .clickFunction('set - transact (not payable)', { types: 'uint256 _p', values: '34' }) - .click('i.savetransaction') + .click('.savetransaction') .waitForElementVisible('[data-id="udappNotify-modal-footer-ok-react"]') .execute(function () { const modalOk = document.querySelector('[data-id="udappNotify-modal-footer-ok-react"]') as any @@ -77,7 +77,7 @@ module.exports = { .selectContract('t2est') .pause(1000) .createContract([]) - .click('i.savetransaction') + .click('.savetransaction') .waitForElementVisible('[data-id="udappNotify-modal-footer-ok-react"]') .execute(function () { const modalOk = document.querySelector('[data-id="udappNotify-modal-footer-ok-react"]') as any @@ -95,6 +95,50 @@ module.exports = { status: 'true Transaction mined and execution succeed', 'decoded input': { 'uint256 _po': '10' } }) + + }, + + 'Run with live "mode"': function (browser: NightwatchBrowser) { + let addressRef: string + browser.addFile('scenario_live_mode.json', { content: JSON.stringify(liveModeScenario, null, '\t') }) + .addFile('scenario_live_mode_storage.sol', { content: testStorageForLiveMode }) + .clickLaunchIcon('solidity') + .click('*[data-id="compilerContainerCompileBtn"]') + .openFile('scenario_live_mode.json') + .clickLaunchIcon('udapp') + .click('*[data-id="deployAndRunClearInstances"]') + .click('*[data-id="runtabLivemodeInput"]') + .click('.runtransaction') + .pause(1000) + .clickInstance(0) + .getAddressAtPosition(0, (address) => { + addressRef = address + }) + .clickFunction('retrieve - call') + .perform((done) => { + browser.verifyCallReturnValue(addressRef, ['', '0:uint256: 350']) + .perform(() => done()) + }) + // change the init state and recompile the same contract. + .openFile('scenario_live_mode_storage.sol') + .setEditorValue(testStorageForLiveMode.replace('number = 350', 'number = 300')) + .pause(5000) + .clickLaunchIcon('solidity') + .click('*[data-id="compilerContainerCompileBtn"]') + .openFile('scenario_live_mode.json') + .clickLaunchIcon('udapp') + .click('*[data-id="deployAndRunClearInstances"]') + .click('.runtransaction') + .pause(5000) + .clickInstance(0) + .getAddressAtPosition(0, (address) => { + addressRef = address + }) + .clickFunction('retrieve - call') + .perform((done) => { + browser.verifyCallReturnValue(addressRef, ['', '0:uint256: 300']) + .perform(() => done()) + }) .end() } } @@ -364,3 +408,91 @@ const scenario = { ] } } + +const liveModeScenario = { + "accounts": { + "account{0}": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4" + }, + "linkReferences": {}, + "transactions": [ + { + "timestamp": 1656329164297, + "record": { + "value": "0", + "parameters": [], + "abi": "0x8b8c9c14c8e1442e90dd6ff82bb9889ccfe5a54d88ef30776f11047ecce5fedb", + "contractName": "Storage", + "bytecode": "608060405234801561001057600080fd5b5060c88061001f6000396000f3fe6080604052348015600f57600080fd5b5060043610604e577c010000000000000000000000000000000000000000000000000000000060003504632e64cec1811460535780636057361d146068575b600080fd5b60005460405190815260200160405180910390f35b60786073366004607a565b600055565b005b600060208284031215608b57600080fd5b503591905056fea264697066735822122091f1bc250ccda7caf2b0d9f67b0314d92233fdb5952b72cece72bd2a5d43cfc264736f6c63430008070033", + "linkReferences": {}, + "name": "", + "inputs": "()", + "type": "constructor", + "from": "account{0}" + } + } + ], + "abis": { + "0x8b8c9c14c8e1442e90dd6ff82bb9889ccfe5a54d88ef30776f11047ecce5fedb": [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "num", + "type": "uint256" + } + ], + "name": "store", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "retrieve", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ] + } +} + +const testStorageForLiveMode = `// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.7.0 <0.9.0; + +/** + * @title Storage + * @dev Store & retrieve value in a variable + * @custom:dev-run-script ./scripts/deploy_with_ethers.ts + */ +contract Storage { + + uint256 number; + + constructor () { + number = 350; + } + + /** + * @dev Store value in variable + * @param num value to store + */ + function store(uint256 num) public { + number = num; + } + + /** + * @dev Return value + * @return value of 'number' + */ + function retrieve() public view returns (uint256){ + return number; + } +}` diff --git a/apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts b/apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts index 7b3315e614..6d87899d79 100644 --- a/apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts +++ b/apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts @@ -32,7 +32,7 @@ module.exports = { 'Should sign message using account key #group2': function (browser: NightwatchBrowser) { browser.waitForElementVisible('*[data-id="settingsRemixRunSignMsg"]') - .click('select[id="selectExEnvOptions"] option[value="vm-berlin"]') + .switchEnvironment('vm-berlin') .pause(2000) .click('*[data-id="settingsRemixRunSignMsg"]') .pause(2000) @@ -198,7 +198,7 @@ module.exports = { .assert.containsText('*[data-id="solidityLocals"]', 'to: 0x6C3CCC7FBA111707D5A1AAF2758E9D4F4AC5E7B1') }, - 'Call web3.eth.getAccounts() using Injected web3 (Metamask)': '' + function (browser: NightwatchBrowser) { + 'Call web3.eth.getAccounts() using Injected Provider (Metamask)': '' + function (browser: NightwatchBrowser) { browser .executeScript('web3.eth.getAccounts()') .pause(2000) diff --git a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts index 30a2913891..7e61167107 100644 --- a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts +++ b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts @@ -181,7 +181,7 @@ module.exports = { .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_new' }) .waitForElementVisible('*[data-id="fileSystem-modal-footer-ok-react"]') .execute(function () { (document.querySelector('[data-id="fileSystem-modal-footer-ok-react"]') as HTMLElement).click() }) - .waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_new"]') + .currentWorkspaceIs('workspace_new') .waitForElementVisible('li[data-id="treeViewLitreeViewItem.deps/remix-tests/remix_tests.sol"]') .waitForElementVisible('li[data-id="treeViewLitreeViewItem.deps/remix-tests/remix_accounts.sol"]') .openFile('.deps/remix-tests/remix_tests.sol') diff --git a/apps/remix-ide-e2e/src/tests/terminal.test.ts b/apps/remix-ide-e2e/src/tests/terminal.test.ts index 40c59875da..c061d8fd39 100644 --- a/apps/remix-ide-e2e/src/tests/terminal.test.ts +++ b/apps/remix-ide-e2e/src/tests/terminal.test.ts @@ -40,7 +40,7 @@ module.exports = { .waitForElementContainsText('*[data-id="terminalJournal"]', 'contract Ballot {', 60000) }, - 'Call web3.eth.getAccounts() using JavaScript VM #group2': function (browser: NightwatchBrowser) { + 'Call web3.eth.getAccounts() using Remix VM #group2': function (browser: NightwatchBrowser) { browser .executeScript('web3.eth.getAccounts()') .waitForElementContainsText('*[data-id="terminalJournal"]', '["0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2","0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db","0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB","0x617F2E2fD72FD9D5503197092aC168c91465E7f2","0x17F6AD8Ef982297579C203069C1DbfFE4348c372","0x5c6B0f7Bf3E7ce046039Bd8FABdfD3f9F5021678","0x03C6FcED478cBbC9a4FAB34eF9f40767739D1Ff7","0x1aE0EA34a72D944a8C7603FfB3eC30a6669E454C","0x0A098Eda01Ce92ff4A4CCb7A4fFFb5A43EBC70DC","0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c","0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C","0x4B0897b0513fdC7C541B6d9D7E929C4e5364D2dB","0x583031D1113aD414F02576BD6afaBfb302140225","0xdD870fA1b7C4700F2BD7f44238821C26f7392148"]') @@ -50,8 +50,8 @@ module.exports = { browser .click('*[data-id="terminalClearConsole"]') // clear the terminal .clickLaunchIcon('udapp') - .click('*[data-id="settingsWeb3Mode"]') - .modalFooterOKClick('envNotification') + .switchEnvironment('External Http Provider') + .modalFooterOKClick('basic-http-provider') .executeScript('web3.eth.getAccounts()') .waitForElementContainsText('*[data-id="terminalJournal"]', '["', 60000) // we check if an array is present, don't need to check for the content .waitForElementContainsText('*[data-id="terminalJournal"]', '"]', 60000) @@ -95,7 +95,7 @@ module.exports = { browser .clickLaunchIcon('settings') .clickLaunchIcon('udapp') - .click('*[data-id="settingsVMLondonMode"]') + .switchEnvironment('vm-london') .click('*[data-id="terminalClearConsole"]') // clear the terminal .clickLaunchIcon('filePanel') .click('*[data-id="treeViewDivtreeViewItem"]') // make sure we create the file at the root folder diff --git a/apps/remix-ide-e2e/src/tests/transactionExecution.test.ts b/apps/remix-ide-e2e/src/tests/transactionExecution.test.ts index 3eac406fcd..842da3b3c7 100644 --- a/apps/remix-ide-e2e/src/tests/transactionExecution.test.ts +++ b/apps/remix-ide-e2e/src/tests/transactionExecution.test.ts @@ -161,7 +161,7 @@ module.exports = { browser .clickLaunchIcon('udapp') .clearTransactions() - .click('*[data-id="settingsVMLondonMode"]') // switch to London fork + .switchEnvironment('vm-london') // switch to London fork .selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c') // this account will be used for this test suite .click('.udapp_contractActionsContainerSingle > button') .clickInstance(0) diff --git a/apps/remix-ide-e2e/src/tests/url.test.ts b/apps/remix-ide-e2e/src/tests/url.test.ts index a201b4ea36..00885338b4 100644 --- a/apps/remix-ide-e2e/src/tests/url.test.ts +++ b/apps/remix-ide-e2e/src/tests/url.test.ts @@ -29,6 +29,7 @@ module.exports = { .click('[for="autoCompile"]') // we set it too false again .click('[for="autoCompile"]') // back to True in the local storage .assert.containsText('*[data-id="compilerContainerCompileBtn"]', 'contract-76747f6e19.sol') + .clickLaunchIcon('filePanel') .currentWorkspaceIs('code-sample') .getEditorValue((content) => { browser.assert.ok(content && content.indexOf( @@ -57,6 +58,7 @@ module.exports = { .url('http://127.0.0.1:8080/#optimize=true&runs=300&evmVersion=istanbul&version=soljson-v0.7.4+commit.3f05b770.js&url=https://github.com/ethereum/remix-project/blob/master/apps/remix-ide/contracts/app/solidity/mode.sol&code=cHJhZ21hIHNvbGlkaXR5ID49MC42LjAgPDAuNy4wOwoKaW1wb3J0ICJodHRwczovL2dpdGh1Yi5jb20vT3BlblplcHBlbGluL29wZW56ZXBwZWxpbi1jb250cmFjdHMvYmxvYi9tYXN0ZXIvY29udHJhY3RzL2FjY2Vzcy9Pd25hYmxlLnNvbCI7Cgpjb250cmFjdCBHZXRQYWlkIGlzIE93bmFibGUgewogIGZ1bmN0aW9uIHdpdGhkcmF3KCkgZXh0ZXJuYWwgb25seU93bmVyIHsKICB9Cn0') .refresh() // we do one reload for making sure we already have the default workspace .pause(5000) + .clickLaunchIcon('filePanel') .currentWorkspaceIs('code-sample') .getEditorValue((content) => { browser.assert.ok(content && content.indexOf( @@ -113,7 +115,7 @@ module.exports = { .url('http://127.0.0.1:8080/#optimize=false&runs=200&evmVersion=null&version=soljson-v0.6.12+commit.27d51765.js&url=https://raw.githubusercontent.com/EthVM/evm-source-verification/main/contracts/1/0x011e5846975c6463a8c6337eecf3cbf64e328884/input.json') .refresh() .pause(5000) - .waitForElementPresent('*[data-id="workspacesSelect"] option[value="code-sample"]') + .switchWorkspace('code-sample') .openFile('@openzeppelin') .openFile('@openzeppelin/contracts') .openFile('@openzeppelin/contracts/access') diff --git a/apps/remix-ide-e2e/src/tests/workspace.test.ts b/apps/remix-ide-e2e/src/tests/workspace.test.ts index 7e7cfbd55e..aa0d2bbaf1 100644 --- a/apps/remix-ide-e2e/src/tests/workspace.test.ts +++ b/apps/remix-ide-e2e/src/tests/workspace.test.ts @@ -38,7 +38,7 @@ module.exports = { .clickLaunchIcon('filePanel') .click('*[data-id="workspaceCreate"]') .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') - .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > span') + .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > button') // eslint-disable-next-line dot-notation .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_remix_default' }) .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') @@ -94,7 +94,7 @@ module.exports = { browser .click('*[data-id="workspaceCreate"]') .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') - .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > span') + .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > button') // eslint-disable-next-line dot-notation .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_blank' }) .click('select[id="wstemplate"]') @@ -115,7 +115,7 @@ module.exports = { browser .click('*[data-id="workspaceCreate"]') .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') - .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > span') + .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > button') // eslint-disable-next-line dot-notation .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_erc20' }) .click('select[id="wstemplate"]') @@ -163,7 +163,7 @@ module.exports = { browser .click('*[data-id="workspaceCreate"]') .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') - .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > span') + .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > button') // eslint-disable-next-line dot-notation .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_erc721' }) .click('select[id="wstemplate"]') @@ -213,7 +213,7 @@ module.exports = { browser .click('*[data-id="workspaceCreate"]') // create workspace_name .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') - .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > span') + .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > button') .click('*[data-id="modalDialogCustomPromptTextCreate"]') .clearValue('*[data-id="modalDialogCustomPromptTextCreate"]') .setValue('*[data-id="modalDialogCustomPromptTextCreate"]', 'workspace_name') @@ -225,7 +225,7 @@ module.exports = { .waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]') .click('*[data-id="workspaceCreate"]') // create workspace_name_1 .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') - .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > span') + .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > button') .click('*[data-id="modalDialogCustomPromptTextCreate"]') .clearValue('*[data-id="modalDialogCustomPromptTextCreate"]') .setValue('*[data-id="modalDialogCustomPromptTextCreate"]', 'workspace_name_1') @@ -235,7 +235,7 @@ module.exports = { .pause(2000) .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]') .pause(2000) - .click('*[data-id="workspacesSelect"] option[value="workspace_name"]') + .switchWorkspace('workspace_name') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]') }, @@ -249,23 +249,23 @@ module.exports = { .setValue('*[data-id="modalDialogCustomPromptTextRename"]', 'workspace_name_renamed') .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') .click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') - .waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') - .click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') + .switchWorkspace('workspace_name_1') .pause(2000) .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]') - .waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_name_renamed"]') - .click('*[data-id="workspacesSelect"] option[value="workspace_name_renamed"]') + .switchWorkspace('workspace_name_renamed') .pause(2000) .waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]') }, 'Should delete a workspace #group1': function (browser: NightwatchBrowser) { browser - .click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') + .switchWorkspace('workspace_name_1') .click('*[data-id="workspaceDelete"]') // delete workspace_name_1 .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') .click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') - .waitForElementNotPresent('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') + .waitForElementVisible('[data-id="workspacesSelect"]') + .click('[data-id="workspacesSelect"]') + .waitForElementNotPresent(`[data-id="dropdown-item-workspace_name_1"]`) .end() }, diff --git a/apps/remix-ide-e2e/src/types/index.d.ts b/apps/remix-ide-e2e/src/types/index.d.ts index f17e6b17de..9ba94f470b 100644 --- a/apps/remix-ide-e2e/src/types/index.d.ts +++ b/apps/remix-ide-e2e/src/types/index.d.ts @@ -62,7 +62,9 @@ declare module 'nightwatch' { clearConsole (this: NightwatchBrowser): NightwatchBrowser clearTransactions (this: NightwatchBrowser): NightwatchBrowser getBrowserLogs (this: NightwatchBrowser): NightwatchBrowser - currentSelectedFileIs (name: string): NightwatchBrowser + currentSelectedFileIs (name: string): NightwatchBrowser, + switchWorkspace: (workspaceName: string) => NightwatchBrowser + switchEnvironment: (provider: string) => NightwatchBrowser } export interface NightwatchBrowser { diff --git a/apps/remix-ide/bin/remix-ide b/apps/remix-ide/bin/remix-ide deleted file mode 100755 index 887bab51fa..0000000000 --- a/apps/remix-ide/bin/remix-ide +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env node -var path = require('path') -var httpServer = require('http-server') -var remixd = require('remixd') - -var server = httpServer.createServer({ - root: path.join(__dirname, '/../') -}) - -var folder = process.argv.length > 2 ? process.argv[2] : process.cwd() - -server.listen(8080, '127.0.0.1', function () {}) -var router = new remixd.Router(65520, remixd.services.sharedFolder, { remixIdeUrl: 'http://localhost:8080' }, (webSocket) => { - remixd.services.sharedFolder.setWebSocket(webSocket) - remixd.services.sharedFolder.setupNotifications(folder) - remixd.services.sharedFolder.sharedFolder(folder, false) -}) - -router.start() - -console.log('\x1b[33m%s\x1b[0m', 'Starting Remix IDE at http://localhost:8080 and sharing ' + folder) diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index 4eb9423d38..d9c93fd4f6 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -28,6 +28,9 @@ import { Blockchain } from './blockchain/blockchain.js' import { HardhatProvider } from './app/tabs/hardhat-provider' import { GanacheProvider } from './app/tabs/ganache-provider' import { FoundryProvider } from './app/tabs/foundry-provider' +import { ExternalHttpProvider } from './app/tabs/external-http-provider' +import { Injected0ptimismProvider } from './app/tabs/injected-optimism-provider' +import { InjectedArbitrumOneProvider } from './app/tabs/injected-arbitrum-one-provider' const isElectron = require('is-electron') @@ -179,6 +182,9 @@ class AppComponent { const hardhatProvider = new HardhatProvider(blockchain) const ganacheProvider = new GanacheProvider(blockchain) const foundryProvider = new FoundryProvider(blockchain) + const externalHttpProvider = new ExternalHttpProvider(blockchain) + const injected0ptimismProvider = new Injected0ptimismProvider(blockchain) + const injectedArbitrumOneProvider = new InjectedArbitrumOneProvider(blockchain) // ----------------- convert offset to line/column service ----------- const offsetToLineColumnConverter = new OffsetToLineColumnConverter() Registry.getInstance().put({ @@ -236,6 +242,9 @@ class AppComponent { hardhatProvider, ganacheProvider, foundryProvider, + externalHttpProvider, + injected0ptimismProvider, + injectedArbitrumOneProvider, this.walkthroughService, search ]) @@ -321,7 +330,8 @@ class AppComponent { filePanel.slitherHandle, linkLibraries, deployLibraries, - openZeppelinProxy + openZeppelinProxy, + run.recorder ]) this.layout.panels = { @@ -355,7 +365,7 @@ class AppComponent { await this.appManager.activatePlugin(['settings', 'config']) await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler']) await this.appManager.activatePlugin(['settings']) - await this.appManager.activatePlugin(['walkthrough','storage', 'search','compileAndRun']) + await this.appManager.activatePlugin(['walkthrough','storage', 'search','compileAndRun', 'recorder']) this.appManager.on( 'filePanel', diff --git a/apps/remix-ide/src/app/files/dgitProvider.js b/apps/remix-ide/src/app/files/dgitProvider.js index 249640a0f3..8b5ce925c5 100644 --- a/apps/remix-ide/src/app/files/dgitProvider.js +++ b/apps/remix-ide/src/app/files/dgitProvider.js @@ -233,12 +233,11 @@ class DGitProvider extends Plugin { return this.calculateLocalStorage() } - async clone (input) { + async clone (input, workspaceName, workspaceExists = false) { const permission = await this.askUserPermission('clone', 'Import multiple files into your workspaces.') if (!permission) return false if (this.calculateLocalStorage() > 10000) throw new Error('The local storage of the browser is full.') - await this.call('filePanel', 'createWorkspace', `workspace_${Date.now()}`, true) - + if (!workspaceExists) await this.call('filePanel', 'createWorkspace', workspaceName || `workspace_${Date.now()}`, true) const cmd = { url: input.url, singleBranch: input.singleBranch, @@ -249,9 +248,11 @@ class DGitProvider extends Plugin { } const result = await git.clone(cmd) - setTimeout(async () => { - await this.call('fileManager', 'refresh') - }, 1000) + if (!workspaceExists) { + setTimeout(async () => { + await this.call('fileManager', 'refresh') + }, 1000) + } return result } diff --git a/apps/remix-ide/src/app/files/fileManager.ts b/apps/remix-ide/src/app/files/fileManager.ts index e7ae4763df..44b604b8d4 100644 --- a/apps/remix-ide/src/app/files/fileManager.ts +++ b/apps/remix-ide/src/app/files/fileManager.ts @@ -19,7 +19,7 @@ const profile = { icon: 'assets/img/fileManager.webp', permission: true, version: packageJson.version, - methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', 'dirList', 'fileList', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile', 'refresh', 'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath', 'saveCurrentFile', 'setBatchFiles'], + methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', 'dirList', 'fileList', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile', 'refresh', 'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath', 'saveCurrentFile', 'setBatchFiles', 'isGitRepo'], kind: 'file-system' } const errorMsg = { @@ -810,6 +810,13 @@ class FileManager extends Plugin { return provider.workspace } } + + async isGitRepo (directory: string): Promise { + const path = directory + '/.git' + const exists = await this.exists(path) + + return exists + } } module.exports = FileManager diff --git a/apps/remix-ide/src/app/plugins/storage.ts b/apps/remix-ide/src/app/plugins/storage.ts index 32d5c90387..cd7ec1e162 100644 --- a/apps/remix-ide/src/app/plugins/storage.ts +++ b/apps/remix-ide/src/app/plugins/storage.ts @@ -15,14 +15,14 @@ export class StoragePlugin extends Plugin { async getStorage() { let storage = null if ('storage' in navigator && 'estimate' in navigator.storage && (window as any).remixFileSystem.name !== 'localstorage') { - storage = navigator.storage.estimate() + storage = await navigator.storage.estimate() } else { storage ={ usage: parseFloat(this.calculateLocalStorage()) * 1000, quota: 5000000, } } - const _paq = window._paq = window._paq || [] + const _paq = (window as any)._paq = (window as any)._paq || [] _paq.push(['trackEvent', 'Storage', 'used', this.formatString(storage)]); return storage } @@ -32,8 +32,8 @@ export class StoragePlugin extends Plugin { } calculateLocalStorage() { - var _lsTotal = 0 - var _xLen; var _x + let _lsTotal = 0 + let _xLen; let _x for (_x in localStorage) { // eslint-disable-next-line no-prototype-builtins if (!localStorage.hasOwnProperty(_x)) { diff --git a/apps/remix-ide/src/app/tabs/abstract-provider.tsx b/apps/remix-ide/src/app/tabs/abstract-provider.tsx index b17f181166..31b1a4e317 100644 --- a/apps/remix-ide/src/app/tabs/abstract-provider.tsx +++ b/apps/remix-ide/src/app/tabs/abstract-provider.tsx @@ -3,21 +3,21 @@ import { AppModal, AlertModal, ModalTypes } from '@remix-ui/app' import { Blockchain } from '../../blockchain/blockchain' import { ethers } from 'ethers' -type JsonDataRequest = { +export type JsonDataRequest = { id: number, jsonrpc: string // version method: string, params: Array, } -type JsonDataResult = { +export type JsonDataResult = { id: number, jsonrpc: string // version result: any } -type RejectRequest = (error: Error) => void -type SuccessRequest = (data: JsonDataResult) => void +export type RejectRequest = (error: Error) => void +export type SuccessRequest = (data: JsonDataResult) => void export abstract class AbstractProvider extends Plugin { provider: ethers.providers.JsonRpcProvider @@ -58,6 +58,20 @@ export abstract class AbstractProvider extends Plugin { modalType: ModalTypes.prompt, okLabel: 'OK', cancelLabel: 'Cancel', + validationFn: (value) => { + if (!value) return { valid: false, message: "value is empty" } + if (value.startsWith('https://') || value.startsWith('http://')) { + return { + valid: true, + message: '' + } + } else { + return { + valid: false, + message: 'the provided value should contain the protocol ( e.g starts with http:// or https:// )' + } + } + }, okFn: (value: string) => { setTimeout(() => resolve(value), 0) }, diff --git a/apps/remix-ide/src/app/tabs/external-http-provider.tsx b/apps/remix-ide/src/app/tabs/external-http-provider.tsx new file mode 100644 index 0000000000..5a22b48ba4 --- /dev/null +++ b/apps/remix-ide/src/app/tabs/external-http-provider.tsx @@ -0,0 +1,41 @@ +import * as packageJson from '../../../../../package.json' +import React from 'react' // eslint-disable-line +import { AbstractProvider } from './abstract-provider' + +const profile = { + name: 'basic-http-provider', + displayName: 'External Http Provider', + kind: 'provider', + description: '', + methods: ['sendAsync'], + version: packageJson.version +} + +export class ExternalHttpProvider extends AbstractProvider { + constructor (blockchain) { + super(profile, blockchain, 'http://127.0.0.1:8545') + } + + body (): JSX.Element { + const thePath = '' + return ( + <> +
+ Note: To use Geth & https://remix.ethereum.org, configure it to allow requests from Remix:(see Geth Docs on rpc server) +
geth --http --http.corsdomain https://remix.ethereum.org
+
+ To run Remix & a local Geth test node, use this command: (see Geth Docs on Dev mode) +
geth --http --http.corsdomain="{window.origin}" --http.api web3,eth,debug,personal,net --vmdebug --datadir {thePath} --dev console
+
+
+ WARNING: It is not safe to use the --http.corsdomain flag with a wildcard: --http.corsdomain * +
+
For more info: Remix Docs on Web3 Provider +
+
+ External HTTP Provider Endpoint +
+ + ) + } +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/tabs/injected-arbitrum-one-provider.tsx b/apps/remix-ide/src/app/tabs/injected-arbitrum-one-provider.tsx new file mode 100644 index 0000000000..283dacdf24 --- /dev/null +++ b/apps/remix-ide/src/app/tabs/injected-arbitrum-one-provider.tsx @@ -0,0 +1,21 @@ +import * as packageJson from '../../../../../package.json' +import { InjectedProvider } from './injected-provider' + +const profile = { + name: 'injected-arbitrum-one-provider', + displayName: 'Injected Arbitrum One Provider', + kind: 'provider', + description: 'injected Arbitrum One Provider', + methods: ['sendAsync'], + version: packageJson.version +} + +export class InjectedArbitrumOneProvider extends InjectedProvider { + + constructor () { + super(profile) + this.chainName = 'Arbitrum One' + this.chainId = '0xa4b1' + this.rpcUrls = ['https://arb1.arbitrum.io/rpc'] + } +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/tabs/injected-optimism-provider.tsx b/apps/remix-ide/src/app/tabs/injected-optimism-provider.tsx new file mode 100644 index 0000000000..64c8c4e91a --- /dev/null +++ b/apps/remix-ide/src/app/tabs/injected-optimism-provider.tsx @@ -0,0 +1,21 @@ +import * as packageJson from '../../../../../package.json' +import { InjectedProvider } from './injected-provider' + +const profile = { + name: 'injected-optimism-provider', + displayName: 'Injected Optimism Provider', + kind: 'provider', + description: 'injected Optimism Provider', + methods: ['sendAsync'], + version: packageJson.version +} + +export class Injected0ptimismProvider extends InjectedProvider { + + constructor () { + super(profile) + this.chainName = 'Optimism' + this.chainId = '0xa' + this.rpcUrls = ['https://mainnet.optimism.io'] + } +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/tabs/injected-provider.tsx b/apps/remix-ide/src/app/tabs/injected-provider.tsx new file mode 100644 index 0000000000..038919206c --- /dev/null +++ b/apps/remix-ide/src/app/tabs/injected-provider.tsx @@ -0,0 +1,75 @@ +import { Plugin } from '@remixproject/engine' +import { JsonDataRequest, RejectRequest, SuccessRequest } from './abstract-provider' +import { ethers } from 'ethers' +import Web3 from 'web3' + +export class InjectedProvider extends Plugin { + provider: any + chainName: string + chainId: string + rpcUrls: Array + + constructor (profile) { + super(profile) + if ((window as any).ethereum) { + this.provider = new Web3((window as any).ethereum) + } + } + + sendAsync (data: JsonDataRequest): Promise { + return new Promise((resolve, reject) => { + this.sendAsyncInternal(data, resolve, reject) + }) + } + + private async sendAsyncInternal (data: JsonDataRequest, resolve: SuccessRequest, reject: RejectRequest): Promise { + // Check the case where current environment is VM on UI and it still sends RPC requests + // This will be displayed on UI tooltip as 'cannot get account list: Environment Updated !!' + if (!this.provider) { + this.call('notification', 'toast', 'No injected provider (e.g Metamask) has been found.') + return reject(new Error('no injected provider found.')) + } + try { + if ((window as any) && typeof (window as any).ethereum.enable === 'function') (window as any).ethereum.enable() + if (!await (window as any).ethereum._metamask.isUnlocked()) this.call('notification', 'toast', 'Please make sure the injected provider is unlocked (e.g Metamask).') + await addL2Network(this.chainName, this.chainId, this.rpcUrls) + const resultData = await this.provider.currentProvider.send(data.method, data.params) + resolve({ jsonrpc: '2.0', result: resultData.result, id: data.id }) + } catch (error) { + reject(error) + } + } +} + +export const addL2Network = async (chainName: string, chainId: string, rpcUrls: Array) => { + try { + await (window as any).ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: chainId }], + }); + } catch (switchError) { + // This error code indicates that the chain has not been added to MetaMask. + if (switchError.code === 4902) { + try { + await (window as any).ethereum.request({ + method: 'wallet_addEthereumChain', + params: [ + { + chainId: chainId, + chainName: chainName, + rpcUrls: rpcUrls, + }, + ], + }); + + await (window as any).ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: chainId }], + }); + } catch (addError) { + // handle "add" error + } + } + // handle other "switch" errors + } +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/tabs/runTab/model/recorder.js b/apps/remix-ide/src/app/tabs/runTab/model/recorder.js index 6178063843..caa758b180 100644 --- a/apps/remix-ide/src/app/tabs/runTab/model/recorder.js +++ b/apps/remix-ide/src/app/tabs/runTab/model/recorder.js @@ -1,16 +1,28 @@ var async = require('async') var ethutil = require('ethereumjs-util') var remixLib = require('@remix-project/remix-lib') +import { Plugin } from '@remixproject/engine' +import * as packageJson from '../../../../.././../../package.json' var EventManager = remixLib.EventManager var format = remixLib.execution.txFormat var txHelper = remixLib.execution.txHelper const helper = require('../../../../lib/helper') +const _paq = window._paq = window._paq || [] //eslint-disable-line + +const profile = { + name: 'recorder', + displayName: 'Recorder', + description: '', + version: packageJson.version, + methods: [ ] +} /** * Record transaction as long as the user create them. */ -class Recorder { +class Recorder extends Plugin { constructor (blockchain) { + super(profile) this.event = new EventManager() this.blockchain = blockchain this.data = { _listen: true, _replay: false, journal: [], _createdContracts: {}, _createdContractsReverse: {}, _usedAccounts: {}, _abis: {}, _contractABIReferences: {}, _linkReferences: {} } @@ -21,7 +33,13 @@ class Recorder { // convert to and from to tokens if (this.data._listen) { - var record = { value, parameters: payLoad.funArgs } + var record = { + value, + inputs: txHelper.serializeInputs(payLoad.funAbi), + parameters: payLoad.funArgs, + name: payLoad.funAbi.name, + type: payLoad.funAbi.type + } if (!to) { var abi = payLoad.contractABI var keccak = ethutil.bufferToHex(ethutil.keccakFromString(JSON.stringify(abi))) @@ -43,10 +61,7 @@ class Recorder { var creationTimestamp = this.data._createdContracts[to] record.to = `created{${creationTimestamp}}` record.abi = this.data._contractABIReferences[creationTimestamp] - } - record.name = payLoad.funAbi.name - record.inputs = txHelper.serializeInputs(payLoad.funAbi) - record.type = payLoad.funAbi.type + } for (var p in record.parameters) { var thisarg = record.parameters[p] var thistimestamp = this.data._createdContracts[thisarg] @@ -169,16 +184,30 @@ class Recorder { /** * run the list of records * + * @param {Object} records * @param {Object} accounts * @param {Object} options * @param {Object} abis + * @param {Object} linkReferences + * @param {Function} confirmationCb + * @param {Function} continueCb + * @param {Function} promptCb + * @param {Function} alertCb + * @param {Function} logCallBack + * @param {Function} liveMode * @param {Function} newContractFn * */ - run (records, accounts, options, abis, linkReferences, confirmationCb, continueCb, promptCb, alertCb, logCallBack, newContractFn) { + run (records, accounts, options, abis, linkReferences, confirmationCb, continueCb, promptCb, alertCb, logCallBack, liveMode, newContractFn) { this.setListen(false) - logCallBack(`Running ${records.length} transaction(s) ...`) - async.eachOfSeries(records, (tx, index, cb) => { + const liveMsg = liveMode ? ' in live mode' : '' + logCallBack(`Running ${records.length} transaction(s)${liveMsg} ...`) + async.eachOfSeries(records, async (tx, index, cb) => { + if (liveMode && tx.record.type === 'constructor') { + // resolve the bytecode using the contract name, this ensure getting the last compiled one. + const data = await this.call('compilerArtefacts', 'getArtefactsByContractName', tx.record.contractName) + tx.record.bytecode = data.artefact.evm.bytecode.object + } var record = this.resolveAddress(tx.record, accounts, options) var abi = abis[tx.record.abi] if (!abi) { @@ -257,8 +286,10 @@ class Recorder { }, () => { this.setListen(true) }) } - runScenario (json, continueCb, promptCb, alertCb, confirmationCb, logCallBack, cb) { + runScenario (liveMode, json, continueCb, promptCb, alertCb, confirmationCb, logCallBack, cb) { + _paq.push(['trackEvent', 'run', 'recorder', 'start']) if (!json) { + _paq.push(['trackEvent', 'run', 'recorder', 'wrong-json']) return cb('a json content must be provided') } if (typeof json === 'string') { @@ -269,12 +300,17 @@ class Recorder { } } + let txArray + let accounts + let options + let abis + let linkReferences try { - var txArray = json.transactions || [] - var accounts = json.accounts || [] - var options = json.options || {} - var abis = json.abis || {} - var linkReferences = json.linkReferences || {} + txArray = json.transactions || [] + accounts = json.accounts || [] + options = json.options || {} + abis = json.abis || {} + linkReferences = json.linkReferences || {} } catch (e) { return cb('Invalid Scenario File. Please try again') } @@ -283,7 +319,7 @@ class Recorder { return } - this.run(txArray, accounts, options, abis, linkReferences, confirmationCb, continueCb, promptCb, alertCb, logCallBack, (abi, address, contractName) => { + this.run(txArray, accounts, options, abis, linkReferences, confirmationCb, continueCb, promptCb, alertCb, logCallBack, liveMode, (abi, address, contractName) => { cb(null, abi, address, contractName) }) } diff --git a/apps/remix-ide/src/app/tabs/theme-module.js b/apps/remix-ide/src/app/tabs/theme-module.js index 2fd9e92e1a..ceb7192317 100644 --- a/apps/remix-ide/src/app/tabs/theme-module.js +++ b/apps/remix-ide/src/app/tabs/theme-module.js @@ -37,7 +37,7 @@ export class ThemeModule extends Plugin { themes.map((theme) => { this.themes[theme.name.toLocaleLowerCase()] = { ...theme, - url: window.location.origin + window.location.pathname + theme.url + url: window.location.origin + ( window.location.pathname.startsWith('/address/') || window.location.pathname.endsWith('.sol') ? '/' : window.location.pathname ) + theme.url } }) this._paq = _paq diff --git a/apps/remix-ide/src/app/udapp/run-tab.js b/apps/remix-ide/src/app/udapp/run-tab.js index 8a1e30324e..c9912ecb4c 100644 --- a/apps/remix-ide/src/app/udapp/run-tab.js +++ b/apps/remix-ide/src/app/udapp/run-tab.js @@ -102,6 +102,7 @@ export class RunTab extends ViewPlugin { await this.call('blockchain', 'addProvider', { name: 'Hardhat Provider', + isInjected: false, provider: { async sendAsync (payload, callback) { try { @@ -116,6 +117,7 @@ export class RunTab extends ViewPlugin { await this.call('blockchain', 'addProvider', { name: 'Ganache Provider', + isInjected: false, provider: { async sendAsync (payload, callback) { try { @@ -130,6 +132,7 @@ export class RunTab extends ViewPlugin { await this.call('blockchain', 'addProvider', { name: 'Foundry Provider', + isInjected: false, provider: { async sendAsync (payload, callback) { try { @@ -144,6 +147,7 @@ export class RunTab extends ViewPlugin { await this.call('blockchain', 'addProvider', { name: 'Wallet Connect', + isInjected: false, provider: { async sendAsync (payload, callback) { try { @@ -155,6 +159,50 @@ export class RunTab extends ViewPlugin { } } }) + + await this.call('blockchain', 'addProvider', { + name: 'External Http Provider', + provider: { + async sendAsync (payload, callback) { + try { + const result = await udapp.call('basic-http-provider', 'sendAsync', payload) + callback(null, result) + } catch (e) { + callback(e) + } + } + } + }) + + await this.call('blockchain', 'addProvider', { + name: 'Optimism Provider', + isInjected: true, + provider: { + async sendAsync (payload, callback) { + try { + const result = await udapp.call('injected-optimism-provider', 'sendAsync', payload) + callback(null, result) + } catch (e) { + callback(e) + } + } + } + }) + + await this.call('blockchain', 'addProvider', { + name: 'Arbitrum One Provider', + isInjected: true, + provider: { + async sendAsync (payload, callback) { + try { + const result = await udapp.call('injected-arbitrum-one-provider', 'sendAsync', payload) + callback(null, result) + } catch (e) { + callback(e) + } + } + } + }) } writeFile (fileName, content) { diff --git a/apps/remix-ide/src/blockchain/blockchain.js b/apps/remix-ide/src/blockchain/blockchain.js index 5f6d3ad6c6..3d6e885cec 100644 --- a/apps/remix-ide/src/blockchain/blockchain.js +++ b/apps/remix-ide/src/blockchain/blockchain.js @@ -464,7 +464,7 @@ export class Blockchain extends Plugin { } /** - * This function send a tx only to javascript VM or testnet, will return an error for the mainnet + * This function send a tx only to Remix VM or testnet, will return an error for the mainnet * SHOULD BE TAKEN CAREFULLY! * * @param {Object} tx - transaction. @@ -530,6 +530,7 @@ export class Blockchain extends Plugin { if (this.transactionContextAPI.getAddress) { return this.transactionContextAPI.getAddress(function (err, address) { if (err) return reject(err) + if (!address) return reject('"from" is not defined. Please make sure an account is selected. If you are using a public node, it is likely that no account will be provided. In that case, add the public node to your injected provider (type Metamask) and use injected provider in Remix.') return resolve(address) }) } @@ -548,9 +549,18 @@ export class Blockchain extends Plugin { const runTransaction = async () => { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { - const fromAddress = await getAccount() - const value = await queryValue() - const gasLimit = await getGasLimit() + let fromAddress + let value + let gasLimit + try { + fromAddress = await getAccount() + value = await queryValue() + gasLimit = await getGasLimit() + } catch (e) { + reject(e) + return + } + const tx = { to: args.to, data: args.data.dataHex, useCall: args.useCall, from: fromAddress, value: value, gasLimit: gasLimit, timestamp: args.data.timestamp } const payLoad = { funAbi: args.data.funAbi, funArgs: args.data.funArgs, contractBytecode: args.data.contractBytecode, contractName: args.data.contractName, contractABI: args.data.contractABI, linkReferences: args.data.linkReferences } diff --git a/apps/remix-ide/src/blockchain/execution-context.js b/apps/remix-ide/src/blockchain/execution-context.js index e570b25056..a6286ed507 100644 --- a/apps/remix-ide/src/blockchain/execution-context.js +++ b/apps/remix-ide/src/blockchain/execution-context.js @@ -155,6 +155,9 @@ export class ExecutionContext { infoCb('No injected Web3 provider found. Make sure your provider (e.g. MetaMask) is active and running (when recently activated you may have to reload the page).') return cb() } else { + if (injectedProvider && injectedProvider._metamask && injectedProvider._metamask.isUnlocked) { + if (!await injectedProvider._metamask.isUnlocked()) this.call('notification', 'toast', 'Please make sure the injected provider is unlocked (e.g Metamask).') + } this.askPermission() this.executionContext = context web3.setProvider(injectedProvider) @@ -164,16 +167,23 @@ export class ExecutionContext { } } - if (context === 'web3') { - confirmCb(cb) - } if (this.customNetWorks[context]) { var network = this.customNetWorks[context] - this.setProviderFromEndpoint(network.provider, { context: network.name }, (error) => { - if (error) infoCb(error) - cb() - }) - } + if (!this.customNetWorks[context].isInjected) { + this.setProviderFromEndpoint(network.provider, { context: network.name }, (error) => { + if (error) infoCb(error) + cb() + }) + } else { + // injected + this.askPermission() + this.executionContext = context + web3.setProvider(network.provider) + await this._updateChainContext() + this.event.trigger('contextChanged', [context]) + return cb() + } + } } currentblockGasLimit () { diff --git a/apps/remix-ide/src/remixAppManager.js b/apps/remix-ide/src/remixAppManager.js index bf8c5dccd3..fbe147a67b 100644 --- a/apps/remix-ide/src/remixAppManager.js +++ b/apps/remix-ide/src/remixAppManager.js @@ -19,7 +19,7 @@ const sensitiveCalls = { } export function isNative(name) { - const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd', 'menuicons', 'solidity', 'solidity-logic', 'solidityStaticAnalysis', 'solidityUnitTesting', 'layout', 'notification', 'hardhat-provider', 'ganache-provider'] + const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd', 'menuicons', 'solidity', 'solidity-logic', 'solidityStaticAnalysis', 'solidityUnitTesting', 'layout', 'notification', 'hardhat-provider', 'ganache-provider', 'foundry-provider', 'basic-http-provider', 'injected-optimism-provider', 'injected-arbitrum-one-provider'] return nativePlugins.includes(name) || requiredModules.includes(name) } diff --git a/libs/remix-core-plugin/src/index.ts b/libs/remix-core-plugin/src/index.ts index 420fe2e6d7..5018bca7e2 100644 --- a/libs/remix-core-plugin/src/index.ts +++ b/libs/remix-core-plugin/src/index.ts @@ -8,3 +8,4 @@ export { GistHandler } from './lib/gist-handler' export * from './types/contract' export { LinkLibraries, DeployLibraries } from './lib/link-libraries' export { OpenZeppelinProxy } from './lib/openzeppelin-proxy' +export { fetchContractFromEtherscan } from './lib/helpers/fetch-etherscan' diff --git a/libs/remix-core-plugin/src/lib/helpers/fetch-etherscan.ts b/libs/remix-core-plugin/src/lib/helpers/fetch-etherscan.ts index 32264c32fa..16f6b284b8 100644 --- a/libs/remix-core-plugin/src/lib/helpers/fetch-etherscan.ts +++ b/libs/remix-core-plugin/src/lib/helpers/fetch-etherscan.ts @@ -1,8 +1,11 @@ -export const fetchContractFromEtherscan = async (plugin, network, contractAddress, targetPath) => { +export const fetchContractFromEtherscan = async (plugin, network, contractAddress, targetPath, key?) => { let data const compilationTargets = {} + let etherscanKey - const etherscanKey = await plugin.call('config', 'getAppParameter', 'etherscan-access-token') + if (!key) etherscanKey = await plugin.call('config', 'getAppParameter', 'etherscan-access-token') + else etherscanKey = key + if (etherscanKey) { const endpoint = network.id == 1 ? 'api.etherscan.io' : 'api-' + network.name + '.etherscan.io' data = await fetch('https://' + endpoint + '/api?module=contract&action=getsourcecode&address=' + contractAddress + '&apikey=' + etherscanKey) @@ -10,7 +13,7 @@ export const fetchContractFromEtherscan = async (plugin, network, contractAddres // etherscan api doc https://docs.etherscan.io/api-endpoints/contracts if (data.message === 'OK' && data.status === "1") { if (data.result.length) { - if (data.result[0].SourceCode === '') throw new Error('contract not verified in Etherscan') + if (data.result[0].SourceCode === '') throw new Error(`contract not verified on Etherscan ${network.name} network`) if (data.result[0].SourceCode.startsWith('{')) { data.result[0].SourceCode = JSON.parse(data.result[0].SourceCode.replace(/(?:\r\n|\r|\n)/g, '').replace(/^{{/,'{').replace(/}}$/,'}')) } diff --git a/libs/remix-lib/src/execution/forkAt.ts b/libs/remix-lib/src/execution/forkAt.ts index c5f1a1e981..8e45ce9acf 100644 --- a/libs/remix-lib/src/execution/forkAt.ts +++ b/libs/remix-lib/src/execution/forkAt.ts @@ -50,6 +50,14 @@ const forks = { { number: 12965000, name: 'london' + }, + { + number: 13773000, + name: 'arrowGlacier' + }, + { + number: 15050000, + name: 'grayGlacier' } ], 3: [ diff --git a/libs/remix-lib/src/execution/txRunnerWeb3.ts b/libs/remix-lib/src/execution/txRunnerWeb3.ts index 4483a41fe0..2300973b3a 100644 --- a/libs/remix-lib/src/execution/txRunnerWeb3.ts +++ b/libs/remix-lib/src/execution/txRunnerWeb3.ts @@ -85,7 +85,7 @@ export class TxRunnerWeb3 { runInNode (from, to, data, value, gasLimit, useCall, timestamp, confirmCb, gasEstimationForceSend, promptCb, callback) { const tx = { from: from, to: to, data: data, value: value } - + if (!from) return callback('the value of "from" is not defined. Please make sure an account is selected.') if (useCall) { tx['gas'] = gasLimit if (this._api && this._api.isVM()) tx['timestamp'] = timestamp diff --git a/libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx b/libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx index 12644f5fa7..17c6f604d6 100644 --- a/libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx +++ b/libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react' -import { ModalDialog, ModalDialogProps } from '@remix-ui/modal-dialog' +import { ModalDialog, ModalDialogProps, ValidationResult } from '@remix-ui/modal-dialog' import { ModalTypes } from '../../types' interface ModalWrapperProps extends ModalDialogProps { @@ -29,12 +29,23 @@ const ModalWrapper = (props: ModalWrapperProps) => { (props.cancelFn) ? props.cancelFn() : props.resolve(false) } - const createModalMessage = (defaultValue: string) => { + const onInputChanged = (event) => { + if (props.validationFn) { + const validation = props.validationFn(event.target.value) + setState(prevState => { + return { ...prevState, message: createModalMessage(props.defaultValue, validation), validation } + }) + } + } + + const createModalMessage = (defaultValue: string, validation: ValidationResult) => { return ( <> {props.message} - - ) + + {!validation.valid && {validation.message}} + + ) } useEffect(() => { @@ -47,13 +58,13 @@ const ModalWrapper = (props: ModalWrapperProps) => { ...props, okFn: onFinishPrompt, cancelFn: onCancelFn, - message: createModalMessage(props.defaultValue) + message: createModalMessage(props.defaultValue, { valid: true }) }) break default: setState({ ...props, - okFn: (onOkFn), + okFn: onOkFn, cancelFn: onCancelFn }) break @@ -67,8 +78,16 @@ const ModalWrapper = (props: ModalWrapperProps) => { } }, [props]) + // reset the message and input if any, so when the modal is shown again it doesn't show the previous value. + const handleHide = () => { + setState(prevState => { + return { ...prevState, message: '' } + }) + props.handleHide() + } + return ( - + ) } export default ModalWrapper diff --git a/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx b/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx index 2aca27c1be..701375265d 100644 --- a/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx +++ b/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx @@ -16,11 +16,11 @@ export const ModalProvider = ({ children = [], reducer = modalReducer, initialSt } const modal = (modalData: AppModal) => { - const { id, title, message, okLabel, okFn, cancelLabel, cancelFn, modalType, defaultValue, hideFn, data } = modalData + const { id, title, message, validationFn, okLabel, okFn, cancelLabel, cancelFn, modalType, defaultValue, hideFn, data } = modalData return new Promise((resolve, reject) => { dispatch({ type: modalActionTypes.setModal, - payload: { id, title, message, okLabel, okFn, cancelLabel, cancelFn, modalType: modalType || ModalTypes.default, defaultValue: defaultValue, hideFn, resolve, next: onNextFn, data } + payload: { id, title, message, okLabel, validationFn, okFn, cancelLabel, cancelFn, modalType: modalType || ModalTypes.default, defaultValue: defaultValue, hideFn, resolve, next: onNextFn, data } }) }) } diff --git a/libs/remix-ui/app/src/lib/remix-app/interface/index.ts b/libs/remix-ui/app/src/lib/remix-app/interface/index.ts index f31536678e..cc0521dd51 100644 --- a/libs/remix-ui/app/src/lib/remix-app/interface/index.ts +++ b/libs/remix-ui/app/src/lib/remix-app/interface/index.ts @@ -1,10 +1,16 @@ import { ModalTypes } from '../types' +export type ValidationResult = { + valid: boolean, + message?: string +} + export interface AppModal { id: string timestamp?: number hide?: boolean title: string + validationFn?: (value: string) => ValidationResult // eslint-disable-next-line no-undef message: string | JSX.Element okLabel: string diff --git a/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts b/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts index a50a3dbd66..51641c654c 100644 --- a/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts +++ b/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts @@ -11,6 +11,7 @@ export const modalReducer = (state: ModalState = ModalInitialState, action: Moda id: action.payload.id || timestamp.toString(), hide: false, title: action.payload.title, + validationFn: action.payload.validationFn, message: action.payload.message, okLabel: action.payload.okLabel, okFn: action.payload.okFn, diff --git a/libs/remix-ui/app/src/lib/remix-app/state/modals.ts b/libs/remix-ui/app/src/lib/remix-app/state/modals.ts index 8332d60120..2b10dccbea 100644 --- a/libs/remix-ui/app/src/lib/remix-app/state/modals.ts +++ b/libs/remix-ui/app/src/lib/remix-app/state/modals.ts @@ -8,6 +8,7 @@ export const ModalInitialState: ModalState = { hide: true, title: '', message: '', + validationFn: () => { return {valid: true, message: ''} }, okLabel: '', okFn: () => { }, cancelLabel: '', diff --git a/libs/remix-ui/debugger-ui/src/lib/debugger-ui.tsx b/libs/remix-ui/debugger-ui/src/lib/debugger-ui.tsx index 25dd08705b..bd8008d0e3 100644 --- a/libs/remix-ui/debugger-ui/src/lib/debugger-ui.tsx +++ b/libs/remix-ui/debugger-ui/src/lib/debugger-ui.tsx @@ -331,7 +331,7 @@ export const DebuggerUI = (props: DebuggerUIProps) => { return { ...prevState, opt: { ...prevState.opt, debugWithGeneratedSources: checked } } }) }} type="checkbox" title="Debug with generated sources" /> - + { state.isLocalNodeUsed &&
{ diff --git a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx index 7787daeddf..46a5e76d9b 100644 --- a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx +++ b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx @@ -416,6 +416,20 @@ export const EditorUI = (props: EditorUIProps) => { editor.addCommand(monacoRef.current.KeyMod.CtrlCmd | monacoRef.current.KeyCode.US_MINUS, () => { editor.updateOptions({ fontSize: editor.getOption(43).fontSize - 1 }) }) + + const editorService = editor._codeEditorService; + const openEditorBase = editorService.openCodeEditor.bind(editorService); + editorService.openCodeEditor = async (input, source) => { + const result = await openEditorBase(input, source) + if (input && input.resource && input.resource.path) { + try { + await props.plugin.call('fileManager', 'open', input.resource.path) + } catch (e) { + console.log(e) + } + } + return result + } } function handleEditorWillMount (monaco) { diff --git a/libs/remix-ui/helper/src/index.ts b/libs/remix-ui/helper/src/index.ts index 36d73ef523..9f050ea8b8 100644 --- a/libs/remix-ui/helper/src/index.ts +++ b/libs/remix-ui/helper/src/index.ts @@ -1,3 +1,4 @@ export * from './lib/remix-ui-helper' export * from './lib/helper-components' -export * from './lib/components/PluginViewWrapper' \ No newline at end of file +export * from './lib/components/PluginViewWrapper' +export * from './lib/components/custom-dropdown' \ No newline at end of file diff --git a/libs/remix-ui/helper/src/lib/components/custom-dropdown.tsx b/libs/remix-ui/helper/src/lib/components/custom-dropdown.tsx new file mode 100644 index 0000000000..5f0696a554 --- /dev/null +++ b/libs/remix-ui/helper/src/lib/components/custom-dropdown.tsx @@ -0,0 +1,42 @@ +// The forwardRef is important!! + +import React, { Ref } from "react" + +// Dropdown needs access to the DOM node in order to position the Menu +export const CustomToggle = React.forwardRef(({ children, onClick, icon, className = '' }: { children: React.ReactNode, onClick: (e) => void, icon: string, className: string }, ref: Ref) => ( + +)) + +// forwardRef again here! +// Dropdown needs access to the DOM of the Menu to measure it +export const CustomMenu = React.forwardRef( + ({ children, style, className, 'aria-labelledby': labeledBy }: { children: React.ReactNode, style?: React.CSSProperties, className: string, 'aria-labelledby'?: string }, ref: Ref) => { + return ( +
+
    + { + children + } +
+
+ ) + }, +) diff --git a/libs/remix-ui/helper/src/lib/components/web3Dialog.tsx b/libs/remix-ui/helper/src/lib/components/web3Dialog.tsx index 30055f322a..3412148086 100644 --- a/libs/remix-ui/helper/src/lib/components/web3Dialog.tsx +++ b/libs/remix-ui/helper/src/lib/components/web3Dialog.tsx @@ -24,10 +24,10 @@ export function Web3ProviderDialog (props: web3ProviderDialogProps) {
WARNING: It is not safe to use the --http.corsdomain flag with a wildcard: --http.corsdomain *
-
For more info: Remix Docs on Web3 Provider +
For more info: Remix Docs on Remix Provider

- Web3 Provider Endpoint + External HTTP Provider Endpoint
(
@@ -54,10 +53,6 @@ export const sourceVerificationNotAvailableToastMsg = () => (
) -export const web3Dialog = (externalEndpoint: string, setWeb3Endpoint: (value: string) => void) => ( - -) - export const envChangeNotification = (env: { context: string, fork: string }, from: string) => (
diff --git a/libs/remix-ui/helper/src/lib/remix-ui-helper.ts b/libs/remix-ui/helper/src/lib/remix-ui-helper.ts index 71abad338d..d9fd9b03b9 100644 --- a/libs/remix-ui/helper/src/lib/remix-ui-helper.ts +++ b/libs/remix-ui/helper/src/lib/remix-ui-helper.ts @@ -47,6 +47,22 @@ export const createNonClashingNameAsync = async (name: string, fileManager, pref return name + counter + prefix + '.' + ext } +export const createNonClashingTitle = async (name: string, fileManager) => { + if (!name) name = 'Undefined' + let _counter + let exist = true + + do { + const isDuplicate = await fileManager.exists(name + (_counter || '')) + + if (isDuplicate) _counter = (_counter || 0) + 1 + else exist = false + } while (exist) + const counter = _counter || '' + + return name + counter +} + export const joinPath = (...paths) => { paths = paths.filter((value) => value !== '').map((path) => path.replace(/^\/|\/$/g, '')) // remove first and last slash) if (paths.length === 1) return paths[0] diff --git a/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx b/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx index 38df2fd8b8..936956c62a 100644 --- a/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx +++ b/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx @@ -96,18 +96,20 @@ export const ModalDialog = (props: ModalDialogProps) => {
{/* todo add autofocus ^^ */} - { props.okLabel && { + if (props.validation && !props.validation.valid) return if (props.okFn) props.okFn() handleHide() }} > {props.okLabel ? props.okLabel : 'OK'} - + } - { props.cancelLabel && { }} > {props.cancelLabel ? props.cancelLabel : 'Cancel'} - + }
diff --git a/libs/remix-ui/modal-dialog/src/lib/types/index.ts b/libs/remix-ui/modal-dialog/src/lib/types/index.ts index a8793971d7..f1ec8c88d9 100644 --- a/libs/remix-ui/modal-dialog/src/lib/types/index.ts +++ b/libs/remix-ui/modal-dialog/src/lib/types/index.ts @@ -1,8 +1,15 @@ +export type ValidationResult = { + valid: boolean, + message?: string +} + /* eslint-disable no-undef */ export interface ModalDialogProps { id: string timestamp?: number, title?: string, + validation?: ValidationResult + validationFn?: (value: string) => ValidationResult message?: string | JSX.Element, okLabel?: string, okFn?: (value?:any) => void, diff --git a/libs/remix-ui/run-tab/src/lib/actions/account.ts b/libs/remix-ui/run-tab/src/lib/actions/account.ts index 9fba9a0723..12b05bec2b 100644 --- a/libs/remix-ui/run-tab/src/lib/actions/account.ts +++ b/libs/remix-ui/run-tab/src/lib/actions/account.ts @@ -1,4 +1,4 @@ -import { shortenAddress, web3Dialog } from "@remix-ui/helper" +import { shortenAddress } from "@remix-ui/helper" import { RunTab } from "../types/run-tab" import { clearInstances, setAccount, setExecEnv } from "./actions" import { displayNotification, displayPopUp, fetchAccountsListFailed, fetchAccountsListRequest, fetchAccountsListSuccess, setExternalEndpoint, setMatchPassphrase, setPassphrase } from "./payload" @@ -74,28 +74,7 @@ const _getProviderDropdownValue = (plugin: RunTab): string => { } export const setExecutionContext = (plugin: RunTab, dispatch: React.Dispatch, executionContext: { context: string, fork: string }) => { - const displayContent = web3Dialog(plugin.REACT_API.externalEndpoint, (endpoint: string) => { - dispatch(setExternalEndpoint(endpoint)) - }) - - plugin.blockchain.changeExecutionContext(executionContext, () => { - plugin.call('notification', 'modal', { - id: 'envNotification', - title: 'External node request', - message: displayContent, - okLabel: 'OK', - cancelLabel: 'Cancel', - okFn: () => { - plugin.blockchain.setProviderFromEndpoint(plugin.REACT_API.externalEndpoint, executionContext, (alertMsg) => { - if (alertMsg) plugin.call('notification', 'toast', alertMsg) - setFinalContext(plugin, dispatch) - }) - }, - cancelFn: () => { - setFinalContext(plugin, dispatch) - } - }) - }, (alertMsg) => { + plugin.blockchain.changeExecutionContext(executionContext, null, (alertMsg) => { plugin.call('notification', 'toast', alertMsg) }, () => { setFinalContext(plugin, dispatch) }) } diff --git a/libs/remix-ui/run-tab/src/lib/actions/index.ts b/libs/remix-ui/run-tab/src/lib/actions/index.ts index 60cfe8513f..5dc0eb2de7 100644 --- a/libs/remix-ui/run-tab/src/lib/actions/index.ts +++ b/libs/remix-ui/run-tab/src/lib/actions/index.ts @@ -54,7 +54,7 @@ export const getExecutionContext = () => getContext(plugin) export const executeTransactions = (instanceIndex: number, lookupOnly: boolean, funcABI: FuncABI, inputsValues: string, contractName: string, contractABI, contract, address, logMsg:string, mainnetPrompt: MainnetPrompt, gasEstimationPrompt: (msg: string) => JSX.Element, passphrasePrompt: (msg: string) => JSX.Element, funcIndex?: number) => runTransactions(plugin, dispatch, instanceIndex, lookupOnly, funcABI, inputsValues, contractName, contractABI, contract, address, logMsg, mainnetPrompt, gasEstimationPrompt, passphrasePrompt, funcIndex) export const loadFromAddress = (contract: ContractData, address: string) => loadAddress(plugin, dispatch, contract, address) export const storeNewScenario = async (prompt: (msg: string, defaultValue: string) => JSX.Element) => storeScenario(plugin, dispatch, prompt) -export const runScenario = (gasEstimationPrompt: (msg: string) => JSX.Element, passphrasePrompt: (msg: string) => JSX.Element, confirmDialogContent: MainnetPrompt) => runCurrentScenario(plugin, dispatch, gasEstimationPrompt, passphrasePrompt, confirmDialogContent) +export const runScenario = (liveMode: boolean, gasEstimationPrompt: (msg: string) => JSX.Element, passphrasePrompt: (msg: string) => JSX.Element, confirmDialogContent: MainnetPrompt) => runCurrentScenario(liveMode, plugin, dispatch, gasEstimationPrompt, passphrasePrompt, confirmDialogContent) export const setScenarioPath = (path: string) => updateScenarioPath(dispatch, path) export const getFuncABIValues = (funcABI: FuncABI) => getFuncABIInputs(plugin, funcABI) export const setNetworkName = (networkName: string) => setNetworkNameFromProvider(dispatch, networkName) diff --git a/libs/remix-ui/run-tab/src/lib/actions/recorder.ts b/libs/remix-ui/run-tab/src/lib/actions/recorder.ts index 7f30ee1759..2bf910650b 100644 --- a/libs/remix-ui/run-tab/src/lib/actions/recorder.ts +++ b/libs/remix-ui/run-tab/src/lib/actions/recorder.ts @@ -36,12 +36,13 @@ export const storeScenario = async (plugin: RunTab, dispatch: React.Dispatch, file: string, gasEstimationPrompt: (msg: string) => JSX.Element, passphrasePrompt: (msg: string) => JSX.Element, confirmDialogContent: MainnetPrompt) => { +const runScenario = (liveMode: boolean, plugin: RunTab, dispatch: React.Dispatch, file: string, gasEstimationPrompt: (msg: string) => JSX.Element, passphrasePrompt: (msg: string) => JSX.Element, confirmDialogContent: MainnetPrompt) => { if (!file) return dispatch(displayNotification('Alert', 'Unable to run scenerio, no specified scenario file', 'OK', null)) plugin.fileManager.readFile(file).then((json) => { // TODO: there is still a UI dependency to remove here, it's still too coupled at this point to remove easily plugin.recorder.runScenario( + liveMode, json, (error, continueTxExecution, cancelCb) => { continueHandler(dispatch, gasEstimationPrompt, error, continueTxExecution, cancelCb) @@ -64,9 +65,9 @@ const runScenario = (plugin: RunTab, dispatch: React.Dispatch, file: string }).catch((error) => dispatch(displayNotification('Alert', error, 'OK', null))) } -export const runCurrentScenario = (plugin: RunTab, dispatch: React.Dispatch, gasEstimationPrompt: (msg: string) => JSX.Element, passphrasePrompt: (msg: string) => JSX.Element, confirmDialogContent: MainnetPrompt) => { +export const runCurrentScenario = (liveMode: boolean, plugin: RunTab, dispatch: React.Dispatch, gasEstimationPrompt: (msg: string) => JSX.Element, passphrasePrompt: (msg: string) => JSX.Element, confirmDialogContent: MainnetPrompt) => { const file = plugin.config.get('currentFile') if (!file) return dispatch(displayNotification('Alert', 'A scenario file has to be selected', 'Ok', null)) - runScenario(plugin, dispatch, file, gasEstimationPrompt, passphrasePrompt, confirmDialogContent) + runScenario(liveMode, plugin, dispatch, file, gasEstimationPrompt, passphrasePrompt, confirmDialogContent) } \ No newline at end of file diff --git a/libs/remix-ui/run-tab/src/lib/components/account.tsx b/libs/remix-ui/run-tab/src/lib/components/account.tsx index f8a74a1a34..1cb7aebc4c 100644 --- a/libs/remix-ui/run-tab/src/lib/components/account.tsx +++ b/libs/remix-ui/run-tab/src/lib/components/account.tsx @@ -160,8 +160,8 @@ export function AccountUI (props: AccountProps) { accounts.map((value, index) => ) } -
- +
+ ) diff --git a/libs/remix-ui/run-tab/src/lib/components/contractDropdownUI.tsx b/libs/remix-ui/run-tab/src/lib/components/contractDropdownUI.tsx index df1831deaa..b61563ef5b 100644 --- a/libs/remix-ui/run-tab/src/lib/components/contractDropdownUI.tsx +++ b/libs/remix-ui/run-tab/src/lib/components/contractDropdownUI.tsx @@ -57,10 +57,7 @@ export function ContractDropdownUI (props: ContractDropdownProps) { content: currentFile }) enableAtAddress(true) - } else if (/.(.sol)$/.exec(currentFile) || - /.(.vy)$/.exec(currentFile) || // vyper - /.(.lex)$/.exec(currentFile) || // lexon - /.(.contract)$/.exec(currentFile)) { + } else if (isContractFile(currentFile)) { setAbiLabel({ display: 'none', content: '' @@ -115,16 +112,23 @@ export function ContractDropdownUI (props: ContractDropdownProps) { } } + const isContractFile = (file) => { + return /.(.sol)$/.exec(file) || + /.(.vy)$/.exec(file) || // vyper + /.(.lex)$/.exec(file) || // lexon + /.(.contract)$/.exec(file) + } + const enableAtAddress = (enable: boolean) => { if (enable) { setAtAddressOptions({ disabled: false, - title: 'Interact with the given contract.' + title: 'Interact with the deployed contract - requires the .abi file or compiled .sol file to be selected in the editor (with the same compiler configuration)' }) } else { setAtAddressOptions({ disabled: true, - title: loadedAddress ? 'âš  Compile *.sol file or select *.abi file.' : 'âš  Compile *.sol file or select *.abi file & then enter the address of deployed contract.' + title: loadedAddress ? 'Compile a *.sol file or select a *.abi file.' : 'To interact with a deployed contract, enter its address and compile its source *.sol file (with the same compiler settings) or select its .abi file in the editor. ' }) } } @@ -133,12 +137,12 @@ export function ContractDropdownUI (props: ContractDropdownProps) { if (enable) { setContractOptions({ disabled: false, - title: 'Select contract for Deploy or At Address.' + title: 'Select a compiled contract to deploy or to use with At Address.' }) } else { setContractOptions({ disabled: true, - title: loadType === 'sol' ? 'âš  Select and compile *.sol file to deploy or access a contract.' : 'âš  Selected *.abi file allows accessing contracts, select and compile *.sol file to deploy and access one.' + title: loadType === 'sol' ? 'Select and compile *.sol file to deploy or access a contract.' : 'When there is a compiled .sol file, the choice of contracts to deploy or to use with AtAddress is made here.' }) } } @@ -214,7 +218,7 @@ export function ContractDropdownUI (props: ContractDropdownProps) {
- { (contractList[currentFile] || []).map((contract, index) => { return }) } @@ -258,7 +262,7 @@ export function ContractDropdownUI (props: ContractDropdownProps) {
: '' }
-
or
+
or
{ const provider = props.providers.providerList.find(exEnv => exEnv.value === env) const fork = provider.fork // can be undefined if connected to an external source (web3 provider / injected) let context = provider.value - context = context.startsWith('vm') ? 'vm' : context // context has to be 'vm', 'web3' or 'injected' + context = context.startsWith('vm') ? 'vm' : context props.setExecutionContext({ context, fork }) } + const currentProvider = props.providers.providerList.find(exEnv => exEnv.value === props.selectedEnv) + const bridges = { + 'Optimism Provider': 'https://www.optimism.io/apps/bridges', + 'Arbitrum One Provider': 'https://bridge.arbitrum.io/' + } + + + const isL2 = (provider) => provider && (provider.value === 'Optimism Provider' || provider.value === 'Arbitrum One Provider') return (
- - + + + + { isL2(currentProvider) && 'L2 - '} + { currentProvider && currentProvider.content } + { currentProvider && bridges[currentProvider.value] && + Click to open a bridge for converting L1 mainnet ETH to the selected network currency. + + }> + + } + + + { + props.providers.providerList.map(({ content, value }, index) => ( + { + handleChangeExEnv(value) + }} + data-id={`dropdown-item-${value}`} + > + { isL2({ value }) && 'L2 - ' }{ content } + + )) + } + + +
) diff --git a/libs/remix-ui/run-tab/src/lib/components/gasPrice.tsx b/libs/remix-ui/run-tab/src/lib/components/gasPrice.tsx index 19e7876a75..a7c5335abd 100644 --- a/libs/remix-ui/run-tab/src/lib/components/gasPrice.tsx +++ b/libs/remix-ui/run-tab/src/lib/components/gasPrice.tsx @@ -10,7 +10,7 @@ export function GasPriceUI (props: GasPriceProps) { return (
- +
) } diff --git a/libs/remix-ui/run-tab/src/lib/components/instanceContainerUI.tsx b/libs/remix-ui/run-tab/src/lib/components/instanceContainerUI.tsx index 5c2800ef4d..6890c53f6d 100644 --- a/libs/remix-ui/run-tab/src/lib/components/instanceContainerUI.tsx +++ b/libs/remix-ui/run-tab/src/lib/components/instanceContainerUI.tsx @@ -12,7 +12,7 @@ export function InstanceContainerUI (props: InstanceContainerProps) { return (
-
-
-
- All transactions (deployed contracts and function executions) can be saved and replayed in - another environment. e.g Transactions created in Javascript VM can be replayed in the Injected Web3. -
-
- - -
+
+
+ + +
+
+ + Save {props.count} transaction(s) as scenario file. + + + }> + + + + Run transaction(s) from the current scenario file. + + + }> + + +
) diff --git a/libs/remix-ui/run-tab/src/lib/components/universalDappUI.tsx b/libs/remix-ui/run-tab/src/lib/components/universalDappUI.tsx index ad3c48cabc..679ab61b15 100644 --- a/libs/remix-ui/run-tab/src/lib/components/universalDappUI.tsx +++ b/libs/remix-ui/run-tab/src/lib/components/universalDappUI.tsx @@ -204,7 +204,7 @@ export function UniversalDappUI (props: UdappProps) { return (
-
+
diff --git a/libs/remix-ui/run-tab/src/lib/components/value.tsx b/libs/remix-ui/run-tab/src/lib/components/value.tsx index 3b2008a5f1..6728639a45 100644 --- a/libs/remix-ui/run-tab/src/lib/components/value.tsx +++ b/libs/remix-ui/run-tab/src/lib/components/value.tsx @@ -58,7 +58,7 @@ export function ValueUI (props: ValueProps) { className="form-control udapp_gasNval udapp_col2" id="value" data-id="dandrValue" - title="Enter the value and choose the unit" + title="Enter an amount and choose its unit" onKeyPress={validateInputKey} onChange={validateValue} value={props.sendValue} diff --git a/libs/remix-ui/run-tab/src/lib/css/run-tab.css b/libs/remix-ui/run-tab/src/lib/css/run-tab.css index 60cf8ad00f..2921381007 100644 --- a/libs/remix-ui/run-tab/src/lib/css/run-tab.css +++ b/libs/remix-ui/run-tab/src/lib/css/run-tab.css @@ -286,7 +286,6 @@ .udapp_instance { display: block; flex-direction: column; - margin-bottom: 12px; background: none; border-radius: 2px; } @@ -530,4 +529,7 @@ text-decoration: none; background-color: #007aa6; } +.udapp_selectExEnvOptions { + width: 100%; +} diff --git a/libs/remix-ui/run-tab/src/lib/reducers/runTab.ts b/libs/remix-ui/run-tab/src/lib/reducers/runTab.ts index 41950fc4e1..2a0ff1c855 100644 --- a/libs/remix-ui/run-tab/src/lib/reducers/runTab.ts +++ b/libs/remix-ui/run-tab/src/lib/reducers/runTab.ts @@ -2,6 +2,9 @@ import { CompilerAbstract } from '@remix-project/remix-solidity-ts' import { ContractData } from '@remix-project/core-plugin' import { DeployMode, DeployOption, DeployOptions } from '../types' import { ADD_DEPLOY_OPTION, ADD_INSTANCE, ADD_PROVIDER, CLEAR_INSTANCES, CLEAR_RECORDER_COUNT, DISPLAY_NOTIFICATION, DISPLAY_POPUP_MESSAGE, FETCH_ACCOUNTS_LIST_FAILED, FETCH_ACCOUNTS_LIST_REQUEST, FETCH_ACCOUNTS_LIST_SUCCESS, FETCH_CONTRACT_LIST_FAILED, FETCH_CONTRACT_LIST_REQUEST, FETCH_CONTRACT_LIST_SUCCESS, FETCH_PROVIDER_LIST_FAILED, FETCH_PROVIDER_LIST_REQUEST, FETCH_PROVIDER_LIST_SUCCESS, HIDE_NOTIFICATION, HIDE_POPUP_MESSAGE, REMOVE_DEPLOY_OPTION, REMOVE_INSTANCE, REMOVE_PROVIDER, RESET_STATE, SET_BASE_FEE_PER_GAS, SET_CONFIRM_SETTINGS, SET_CURRENT_CONTRACT, SET_CURRENT_FILE, SET_DECODED_RESPONSE, SET_DEPLOY_OPTIONS, SET_EXECUTION_ENVIRONMENT, SET_EXTERNAL_WEB3_ENDPOINT, SET_GAS_LIMIT, SET_GAS_PRICE, SET_GAS_PRICE_STATUS, SET_IPFS_CHECKED_STATE, SET_LOAD_TYPE, SET_MATCH_PASSPHRASE, SET_MAX_FEE, SET_MAX_PRIORITY_FEE, SET_NETWORK_NAME, SET_PASSPHRASE, SET_PATH_TO_SCENARIO, SET_PERSONAL_MODE, SET_RECORDER_COUNT, SET_SELECTED_ACCOUNT, SET_SEND_UNIT, SET_SEND_VALUE, SET_TX_FEE_CONTENT } from '../constants' +import Web3 from 'web3' + +declare const window: any interface Action { type: string payload: any @@ -115,30 +118,23 @@ export const runTabInitialState: RunTabState = { providerList: [{ id: 'vm-mode-london', dataId: 'settingsVMLondonMode', - title: 'Execution environment does not connect to any node, everything is local and in memory only.', + title: 'Execution environment is local to Remix. Data is only saved to browser memory and will vanish upon reload.', value: 'vm-london', fork: 'london', - content: 'JavaScript VM (London)' + content: 'Remix VM (London)' }, { id: 'vm-mode-berlin', dataId: 'settingsVMBerlinMode', - title: 'Execution environment does not connect to any node, everything is local and in memory only.', + title: 'Execution environment is local to Remix. Data is only saved to browser memory and will vanish upon reload.', value: 'vm-berlin', fork: 'berlin', - content: 'JavaScript VM (Berlin)' + content: 'Remix VM (Berlin)' }, { id: 'injected-mode', dataId: 'settingsInjectedMode', title: 'Execution environment has been provided by Metamask or similar provider.', value: 'injected', - content: 'Injected Web3' - }, { - id: 'web3-mode', - dataId: 'settingsWeb3Mode', - title: `Execution environment connects to node at localhost (or via IPC if available), transactions will be sent to the network and can cause loss of money or worse! - If this page is served via https and you access your node via http, it might not work. In this case, try cloning the repository and serving it via http.`, - value: 'web3', - content: 'Web3 Provider' + content: `Injected Provider${(window && window.ethereum && window.ethereum.isMetaMask) ? ' - Metamask' : ''}` }], isRequesting: false, isSuccessful: false, diff --git a/libs/remix-ui/run-tab/src/lib/types/blockchain.d.ts b/libs/remix-ui/run-tab/src/lib/types/blockchain.d.ts index 1b2f7d442d..1d6e7a5edc 100644 --- a/libs/remix-ui/run-tab/src/lib/types/blockchain.d.ts +++ b/libs/remix-ui/run-tab/src/lib/types/blockchain.d.ts @@ -1,3 +1,6 @@ +import { Plugin } from "@remixproject/engine/lib/abstract"; +import { ExecutionContext } from "./execution-context"; +import { EventEmitter } from "events"; export class Blockchain extends Plugin { constructor(config: any); event: any; @@ -70,7 +73,7 @@ export class Blockchain extends Plugin { getBalanceInEther(address: any, cb: any): void; pendingTransactionsCount(): number; /** - * This function send a tx only to javascript VM or testnet, will return an error for the mainnet + * This function send a tx only to Remix VM or testnet, will return an error for the mainnet * SHOULD BE TAKEN CAREFULLY! * * @param {Object} tx - transaction. @@ -78,6 +81,4 @@ export class Blockchain extends Plugin { sendTransaction(tx: any): any; runTx(args: any, confirmationCb: any, continueCb: any, promptCb: any, cb: any): void; } -import { Plugin } from "@remixproject/engine/lib/abstract"; -import { ExecutionContext } from "./execution-context"; -import { EventEmitter } from "events"; + diff --git a/libs/remix-ui/run-tab/src/lib/types/index.ts b/libs/remix-ui/run-tab/src/lib/types/index.ts index 3e162ea9d6..c304d8815f 100644 --- a/libs/remix-ui/run-tab/src/lib/types/index.ts +++ b/libs/remix-ui/run-tab/src/lib/types/index.ts @@ -166,7 +166,7 @@ export interface ContractDropdownProps { export interface RecorderProps { storeScenario: (prompt: (msg: string, defaultValue: string) => JSX.Element) => void, - runCurrentScenario: (gasEstimationPrompt: (msg: string) => JSX.Element, passphrasePrompt: (msg: string) => JSX.Element, confirmDialogContent: MainnetPrompt) => void, + runCurrentScenario: (liveMode: boolean, gasEstimationPrompt: (msg: string) => JSX.Element, passphrasePrompt: (msg: string) => JSX.Element, confirmDialogContent: MainnetPrompt) => void, mainnetPrompt: MainnetPrompt, gasEstimationPrompt: (msg: string) => JSX.Element, passphrasePrompt: (msg: string) => JSX.Element, diff --git a/libs/remix-ui/run-tab/src/lib/types/recorder.d.ts b/libs/remix-ui/run-tab/src/lib/types/recorder.d.ts index 44f79bda4f..3c4ec18c20 100644 --- a/libs/remix-ui/run-tab/src/lib/types/recorder.d.ts +++ b/libs/remix-ui/run-tab/src/lib/types/recorder.d.ts @@ -9,7 +9,7 @@ export class Recorder { getAll: () => void; clearAll: () => void; run: (records, accounts, options, abis, linkReferences, confirmationCb, continueCb, promptCb, alertCb, logCallBack, newContractFn) => void - runScenario: (json, continueCb, promptCb, alertCb, confirmationCb, logCallBack, cb) => void + runScenario: (liveMode, json, continueCb, promptCb, alertCb, confirmationCb, logCallBack, cb) => void } import { Blockchain } from "./blockchain"; diff --git a/libs/remix-ui/search/src/lib/reducers/Reducer.ts b/libs/remix-ui/search/src/lib/reducers/Reducer.ts index 3e83b75aea..8ff867d1c8 100644 --- a/libs/remix-ui/search/src/lib/reducers/Reducer.ts +++ b/libs/remix-ui/search/src/lib/reducers/Reducer.ts @@ -46,7 +46,7 @@ export const SearchReducer = (state: SearchState = SearchingInitialState, action run: true } case 'SET_UNDO_ENABLED': - if(state.undoBuffer[`${action.payload.workspace}/${action.payload.path}`]){ + if(action.payload.workspace && state.undoBuffer[`${action.payload.workspace}/${action.payload.path}`]){ state.undoBuffer[`${action.payload.workspace}/${action.payload.path}`].enabled = (action.payload.content === state.undoBuffer[`${action.payload.workspace}/${action.payload.path}`].newContent) state.undoBuffer[`${action.payload.workspace}/${action.payload.path}`].visible = (action.payload.content !== state.undoBuffer[`${action.payload.workspace}/${action.payload.path}`].oldContent) } diff --git a/libs/remix-ui/settings/src/lib/constants.ts b/libs/remix-ui/settings/src/lib/constants.ts index ac7e6d1069..4062c68657 100644 --- a/libs/remix-ui/settings/src/lib/constants.ts +++ b/libs/remix-ui/settings/src/lib/constants.ts @@ -11,9 +11,9 @@ export const etherscanTokenTitle = 'EtherScan Access Token' export const etherscanTokenLink = 'https://etherscan.io/myapikey' export const etherscanAccessTokenText = 'Manage the api key used to interact with Etherscan.' export const etherscanAccessTokenText2 = 'Go to Etherscan api key page (link below) to create a new api key and save it in Remix.' -export const ethereunVMText = 'Always use Javascript VM at load' +export const ethereunVMText = 'Always use Remix VM at load' export const wordWrapText = 'Word wrap in editor' -export const enablePersonalModeText = ' Enable Personal Mode for web3 provider. Transaction sent over Web3 will use the web3.personal API.\n' +export const enablePersonalModeText = ' Enable Personal Mode for Remix Provider. Transaction sent over Web3 will use the web3.personal API.\n' export const matomoAnalytics = 'Enable Matomo Analytics. We do not collect personally identifiable information (PII). The info is used to improve the site’s UX & UI. See more about ' export const swarmSettingsTitle = 'Swarm Settings' export const swarmSettingsText = 'Swarm Settings' diff --git a/libs/remix-ui/settings/src/lib/github-settings.tsx b/libs/remix-ui/settings/src/lib/github-settings.tsx new file mode 100644 index 0000000000..e7c9c3f2ce --- /dev/null +++ b/libs/remix-ui/settings/src/lib/github-settings.tsx @@ -0,0 +1,81 @@ +import { CopyToClipboard } from '@remix-ui/clipboard' +import React, { useEffect, useState } from 'react' +import { GithubSettingsProps } from '../types' + +export function GithubSettings (props: GithubSettingsProps) { + const [githubToken, setGithubToken] = useState("") + const [githubUserName, setGithubUsername] = useState("") + const [githubEmail, setGithubEmail] = useState("") + + useEffect(() => { + if (props.config) { + const githubToken = props.config.get('settings/gist-access-token') + const githubUserName = props.config.get('settings/github-user-name') + const githubEmail = props.config.get('settings/github-email') + + setGithubToken(githubToken) + setGithubUsername(githubUserName) + setGithubEmail(githubEmail) + } + }, [props.config]) + + const handleChangeTokenState = (event) => { + setGithubToken(event.target.value) + } + + const handleChangeUserNameState = (event) => { + setGithubUsername(event.target.value) + } + + const handleChangeEmailState = (event) => { + setGithubEmail(event.target.value) + } + + // api key settings + const saveGithubToken = () => { + props.saveTokenToast(githubToken, githubUserName, githubEmail) + } + + const removeToken = () => { + setGithubToken('') + setGithubUsername('') + setGithubEmail('') + props.removeTokenToast() + } + + return ( +
+
+
GitHub Credentials
+

Manage your GitHub credentials used to publish to Gist and retrieve GitHub contents.

+

Go to github token page (link below) to create a new token and save it in Remix. Make sure this token has only \'create gist\' permission.

+

https://github.com/settings/tokens

+
+ +
+ handleChangeTokenState(e)} value={ githubToken } /> +
+ +
+
+
+
+ +
+ handleChangeUserNameState(e)} value={ githubUserName } /> +
+
+
+ +
+ handleChangeEmailState(e)} value={ githubEmail } /> +
+ + +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx index 2de3ecd67b..19655dd281 100644 --- a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx +++ b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx @@ -8,6 +8,7 @@ import { ethereumVM, generateContractMetadat, personal, textWrapEventAction, use import { initialState, toastInitialState, toastReducer, settingReducer } from './settingsReducer' import { Toaster } from '@remix-ui/toaster'// eslint-disable-line import { RemixUiThemeModule, ThemeModule} from '@remix-ui/theme-module' +import { GithubSettings } from './github-settings' /* eslint-disable-next-line */ export interface RemixUiSettingsProps { @@ -347,7 +348,19 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
{state.message ? : null} {generalConfig()} - {token('gist')} + { + saveTokenToast(props.config, dispatchToast, githubToken, "gist-access-token") + saveTokenToast(props.config, dispatchToast, githubUserName, "github-user-name") + saveTokenToast(props.config, dispatchToast, githubEmail, "github-email") + }} + removeTokenToast={() => { + removeTokenToast(props.config, dispatchToast, "gist-access-token") + removeTokenToast(props.config, dispatchToast, "github-user-name") + removeTokenToast(props.config, dispatchToast, "github-email") + }} + config={props.config} + /> {token('etherscan')} {swarmSettings()} {ipfsSettings()} diff --git a/libs/remix-ui/settings/src/lib/settingsAction.ts b/libs/remix-ui/settings/src/lib/settingsAction.ts index b7a7eddd95..fc3bed1f4d 100644 --- a/libs/remix-ui/settings/src/lib/settingsAction.ts +++ b/libs/remix-ui/settings/src/lib/settingsAction.ts @@ -43,12 +43,12 @@ export const useMatomoAnalytics = (config, checked, dispatch) => { export const saveTokenToast = (config, dispatch, tokenValue, key) => { config.set('settings/' + key, tokenValue) - dispatch({ type: 'save', payload: { message: 'Access token has been saved' } }) + dispatch({ type: 'save', payload: { message: 'GitHub credentials updated' } }) } export const removeTokenToast = (config, dispatch, key) => { config.set('settings/' + key, '') - dispatch({ type: 'removed', payload: { message: 'Access token removed' } }) + dispatch({ type: 'removed', payload: { message: 'GitHub credentials removed' } }) } export const saveSwarmSettingsToast = (config, dispatch, privateBeeAddress, postageStampId) => { diff --git a/libs/remix-ui/settings/src/types/index.ts b/libs/remix-ui/settings/src/types/index.ts new file mode 100644 index 0000000000..5b4af8dfc1 --- /dev/null +++ b/libs/remix-ui/settings/src/types/index.ts @@ -0,0 +1,12 @@ +export interface GithubSettingsProps { + saveTokenToast: (githubToken: string, githubUserName: string, githubEmail: string) => void, + removeTokenToast: () => void, + config: { + exists: (key: string) => boolean, + get: (key: string) => string, + set: (key: string, content: string) => void, + clear: () => void, + getUnpersistedProperty: (key: string) => void, + setUnpersistedProperty: (key: string, value: string) => void + } +} diff --git a/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx b/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx index bc06aae822..cb27b4d122 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx +++ b/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx @@ -33,6 +33,7 @@ export const CompilerContainer = (props: CompilerContainerProps) => { configurationSettings, isHardhatProject, isTruffleProject, + isFoundryProject, workspaceName, configFilePath, setConfigFilePath, @@ -71,7 +72,10 @@ export const CompilerContainer = (props: CompilerContainerProps) => { api.setAppParameter('configFilePath', defaultPath) if (state.useFileConfiguration) { api.fileExists(defaultPath).then((exists) => { - if (!exists && state.useFileConfiguration) createNewConfigFile() + if (!exists && state.useFileConfiguration) { + configFilePathInput.current.value = defaultPath + createNewConfigFile() + } }) } setShowFilePathInput(false) @@ -90,8 +94,9 @@ export const CompilerContainer = (props: CompilerContainerProps) => { useEffect(() => { const listener = (event) => { - if (configFilePathInput.current !== event.target) { + if (configFilePathInput.current !== event.target && event.target.innerText !== "Create") { setShowFilePathInput(false) + configFilePathInput.current.value = "" return; } }; @@ -241,7 +246,16 @@ export const CompilerContainer = (props: CompilerContainerProps) => { if (filePath === '') filePath = defaultPath if (!filePath.endsWith('.json')) filePath = filePath + '.json' - await api.writeFile(filePath, configFileContent) + let compilerConfig = configFileContent + if (isFoundryProject && !compilerConfig.includes('remappings')) { + const config = JSON.parse(compilerConfig) + config.settings.remappings = [ + 'ds-test/=lib/forge-std/lib/ds-test/src/', + 'forge-std/=lib/forge-std/src/' + ] + compilerConfig = JSON.stringify(config, null, '\t') + } + await api.writeFile(filePath, compilerConfig) api.setAppParameter('configFilePath', filePath) setConfigFilePath(filePath) compileTabLogic.setConfigFilePath(filePath) @@ -754,7 +768,7 @@ export const CompilerContainer = (props: CompilerContainerProps) => {
- handleLanguageChange(e.target.value)} disabled={state.useFileConfiguration} value={state.language} className="custom-select" id="compilierLanguageSelector" title="Language specification available from Compiler >= v0.5.7"> @@ -789,7 +803,7 @@ export const CompilerContainer = (props: CompilerContainerProps) => {
{ (!showFilePathInput && state.useFileConfiguration) && {} : openFile} className="py-2 remixui_compilerConfigPath" >{configFilePath === '' ? 'No file selected.' : configFilePath} } @@ -861,7 +875,7 @@ export const CompilerContainer = (props: CompilerContainerProps) => { }> - '@custom:dev-run-script file_path'} direction='top'> + '@custom:dev-run-script file_path'} direction='top'> diff --git a/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts b/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts index 7497a529ae..10e08f86ed 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts +++ b/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts @@ -131,6 +131,12 @@ export class CompileTabLogic { } else return false } + async isFoundryProject () { + if (this.api.getFileManagerMode() === 'localhost') { + return await this.api.fileExists('foundry.toml') + } else return false + } + runCompiler (externalCompType) { try { if (this.api.getFileManagerMode() === 'localhost') { diff --git a/libs/remix-ui/solidity-compiler/src/lib/solidity-compiler.tsx b/libs/remix-ui/solidity-compiler/src/lib/solidity-compiler.tsx index 827dea9f3f..3a9ce290e8 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/solidity-compiler.tsx +++ b/libs/remix-ui/solidity-compiler/src/lib/solidity-compiler.tsx @@ -13,6 +13,7 @@ export const SolidityCompiler = (props: SolidityCompilerProps) => { const [state, setState] = useState({ isHardhatProject: false, isTruffleProject: false, + isFoundryProject: false, workspaceName: '', currentFile, configFilePath: 'compiler_config.json', @@ -67,9 +68,10 @@ export const SolidityCompiler = (props: SolidityCompilerProps) => { api.onSetWorkspace = async (isLocalhost: boolean, workspaceName: string) => { const isHardhat = isLocalhost && await compileTabLogic.isHardhatProject() - const isTruffle = await compileTabLogic.isTruffleProject() + const isTruffle = isLocalhost && await compileTabLogic.isTruffleProject() + const isFoundry = isLocalhost && await compileTabLogic.isFoundryProject() setState(prevState => { - return { ...prevState, currentFile, isHardhatProject: isHardhat, workspaceName: workspaceName, isTruffleProject: isTruffle } + return { ...prevState, currentFile, isHardhatProject: isHardhat, workspaceName: workspaceName, isTruffleProject: isTruffle, isFoundryProject: isFoundry } }) } @@ -171,6 +173,7 @@ export const SolidityCompiler = (props: SolidityCompilerProps) => { isHardhatProject={state.isHardhatProject} workspaceName={state.workspaceName} isTruffleProject={state.isTruffleProject} + isFoundryProject={state.isFoundryProject} compileTabLogic={compileTabLogic} tooltip={toast} modal={modal} diff --git a/libs/remix-ui/solidity-compiler/src/lib/types/index.ts b/libs/remix-ui/solidity-compiler/src/lib/types/index.ts index 38f4edb61e..4fd80bcf7d 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/types/index.ts +++ b/libs/remix-ui/solidity-compiler/src/lib/types/index.ts @@ -11,6 +11,7 @@ export interface CompilerContainerProps { compileTabLogic: CompileTabLogic, isHardhatProject: boolean, isTruffleProject: boolean, + isFoundryProject: boolean, workspaceName: string, tooltip: (message: string | JSX.Element) => void, modal: (title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => void, diff --git a/libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx b/libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx index 30f1b2e0df..8f55fe2ea7 100644 --- a/libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx +++ b/libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx @@ -677,7 +677,7 @@ export const SolidityUnitTesting = (props: Record) => { // eslint-d