Merge branch 'master' into badges

pull/5370/head
Rob 2 years ago committed by GitHub
commit 4224f21dfa
  1. 4
      .prettierignore
  2. 3
      .prettierrc
  3. 27
      README.md
  4. 18
      apps/remix-ide-e2e/src/commands/currentWorkspaceIs.ts
  5. 18
      apps/remix-ide-e2e/src/commands/switchEnvironment.ts
  6. 18
      apps/remix-ide-e2e/src/commands/switchWorkspace.ts
  7. 14
      apps/remix-ide-e2e/src/tests/ballot.test.ts
  8. 6
      apps/remix-ide-e2e/src/tests/ballot_0_4_11.test.ts
  9. 2
      apps/remix-ide-e2e/src/tests/debugger.test.ts
  10. 4
      apps/remix-ide-e2e/src/tests/generalSettings.test.ts
  11. 4
      apps/remix-ide-e2e/src/tests/migrateFileSystem.test.ts
  12. 12
      apps/remix-ide-e2e/src/tests/plugin_api.ts
  13. 11
      apps/remix-ide-e2e/src/tests/providers.test.ts
  14. 136
      apps/remix-ide-e2e/src/tests/recorder.test.ts
  15. 4
      apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts
  16. 2
      apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts
  17. 8
      apps/remix-ide-e2e/src/tests/terminal.test.ts
  18. 2
      apps/remix-ide-e2e/src/tests/transactionExecution.test.ts
  19. 4
      apps/remix-ide-e2e/src/tests/url.test.ts
  20. 26
      apps/remix-ide-e2e/src/tests/workspace.test.ts
  21. 4
      apps/remix-ide-e2e/src/types/index.d.ts
  22. 21
      apps/remix-ide/bin/remix-ide
  23. 14
      apps/remix-ide/src/app.js
  24. 13
      apps/remix-ide/src/app/files/dgitProvider.js
  25. 9
      apps/remix-ide/src/app/files/fileManager.ts
  26. 8
      apps/remix-ide/src/app/plugins/storage.ts
  27. 22
      apps/remix-ide/src/app/tabs/abstract-provider.tsx
  28. 41
      apps/remix-ide/src/app/tabs/external-http-provider.tsx
  29. 21
      apps/remix-ide/src/app/tabs/injected-arbitrum-one-provider.tsx
  30. 21
      apps/remix-ide/src/app/tabs/injected-optimism-provider.tsx
  31. 75
      apps/remix-ide/src/app/tabs/injected-provider.tsx
  32. 68
      apps/remix-ide/src/app/tabs/runTab/model/recorder.js
  33. 2
      apps/remix-ide/src/app/tabs/theme-module.js
  34. 48
      apps/remix-ide/src/app/udapp/run-tab.js
  35. 18
      apps/remix-ide/src/blockchain/blockchain.js
  36. 26
      apps/remix-ide/src/blockchain/execution-context.js
  37. 2
      apps/remix-ide/src/remixAppManager.js
  38. 1
      libs/remix-core-plugin/src/index.ts
  39. 9
      libs/remix-core-plugin/src/lib/helpers/fetch-etherscan.ts
  40. 8
      libs/remix-lib/src/execution/forkAt.ts
  41. 2
      libs/remix-lib/src/execution/txRunnerWeb3.ts
  42. 33
      libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx
  43. 4
      libs/remix-ui/app/src/lib/remix-app/context/provider.tsx
  44. 6
      libs/remix-ui/app/src/lib/remix-app/interface/index.ts
  45. 1
      libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts
  46. 1
      libs/remix-ui/app/src/lib/remix-app/state/modals.ts
  47. 2
      libs/remix-ui/debugger-ui/src/lib/debugger-ui.tsx
  48. 14
      libs/remix-ui/editor/src/lib/remix-ui-editor.tsx
  49. 3
      libs/remix-ui/helper/src/index.ts
  50. 42
      libs/remix-ui/helper/src/lib/components/custom-dropdown.tsx
  51. 4
      libs/remix-ui/helper/src/lib/components/web3Dialog.tsx
  52. 5
      libs/remix-ui/helper/src/lib/helper-components.tsx
  53. 16
      libs/remix-ui/helper/src/lib/remix-ui-helper.ts
  54. 10
      libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx
  55. 7
      libs/remix-ui/modal-dialog/src/lib/types/index.ts
  56. 25
      libs/remix-ui/run-tab/src/lib/actions/account.ts
  57. 2
      libs/remix-ui/run-tab/src/lib/actions/index.ts
  58. 7
      libs/remix-ui/run-tab/src/lib/actions/recorder.ts
  59. 4
      libs/remix-ui/run-tab/src/lib/components/account.tsx
  60. 24
      libs/remix-ui/run-tab/src/lib/components/contractDropdownUI.tsx
  61. 64
      libs/remix-ui/run-tab/src/lib/components/environment.tsx
  62. 2
      libs/remix-ui/run-tab/src/lib/components/gasPrice.tsx
  63. 2
      libs/remix-ui/run-tab/src/lib/components/instanceContainerUI.tsx
  64. 49
      libs/remix-ui/run-tab/src/lib/components/recorderCardUI.tsx
  65. 2
      libs/remix-ui/run-tab/src/lib/components/universalDappUI.tsx
  66. 2
      libs/remix-ui/run-tab/src/lib/components/value.tsx
  67. 4
      libs/remix-ui/run-tab/src/lib/css/run-tab.css
  68. 20
      libs/remix-ui/run-tab/src/lib/reducers/runTab.ts
  69. 9
      libs/remix-ui/run-tab/src/lib/types/blockchain.d.ts
  70. 2
      libs/remix-ui/run-tab/src/lib/types/index.ts
  71. 2
      libs/remix-ui/run-tab/src/lib/types/recorder.d.ts
  72. 2
      libs/remix-ui/search/src/lib/reducers/Reducer.ts
  73. 4
      libs/remix-ui/settings/src/lib/constants.ts
  74. 81
      libs/remix-ui/settings/src/lib/github-settings.tsx
  75. 15
      libs/remix-ui/settings/src/lib/remix-ui-settings.tsx
  76. 4
      libs/remix-ui/settings/src/lib/settingsAction.ts
  77. 12
      libs/remix-ui/settings/src/types/index.ts
  78. 26
      libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx
  79. 6
      libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts
  80. 7
      libs/remix-ui/solidity-compiler/src/lib/solidity-compiler.tsx
  81. 1
      libs/remix-ui/solidity-compiler/src/lib/types/index.ts
  82. 2
      libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx
  83. 2
      libs/remix-ui/terminal/src/lib/components/RenderCall.tsx
  84. 2
      libs/remix-ui/terminal/src/lib/components/RenderKnownTransactions.tsx
  85. 2
      libs/remix-ui/terminal/src/lib/components/RenderUnknownTransactions.tsx
  86. 12
      libs/remix-ui/tooltip-popup/.babelrc
  87. 18
      libs/remix-ui/tooltip-popup/.eslintrc.json
  88. 7
      libs/remix-ui/tooltip-popup/README.md
  89. 1
      libs/remix-ui/tooltip-popup/src/index.ts
  90. 0
      libs/remix-ui/tooltip-popup/src/lib/tooltip-popup.module.css
  91. 27
      libs/remix-ui/tooltip-popup/src/lib/tooltip-popup.tsx
  92. 6
      libs/remix-ui/tooltip-popup/src/types/index.ts
  93. 20
      libs/remix-ui/tooltip-popup/tsconfig.json
  94. 13
      libs/remix-ui/tooltip-popup/tsconfig.lib.json
  95. 102
      libs/remix-ui/workspace/src/lib/actions/index.ts
  96. 24
      libs/remix-ui/workspace/src/lib/actions/payload.ts
  97. 71
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  98. 5
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  99. 38
      libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css
  100. 9
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,4 +0,0 @@
# Add files here to ignore them from prettier formatting
/dist
/coverage

@ -1,3 +0,0 @@
{
"singleQuote": true
}

@ -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)
<p align="center">
<img src="./apps/remix-ide/src/assets/img/icon.png" alt="Remix Logo" width="200"/>
</p>
<h3 align="center">Remix Project</h3>
<div align="center">
[![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)
</div>
## 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

@ -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
}
}

@ -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

@ -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

@ -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)

@ -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()
})

@ -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()

@ -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"]', '')
},

@ -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) {

@ -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')

@ -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 (')

@ -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;
}
}`

@ -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)

@ -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')

@ -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

@ -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)

@ -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')

@ -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()
},

@ -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 {

@ -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)

@ -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',

@ -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
}

@ -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<boolean> {
const path = directory + '/.git'
const exists = await this.exists(path)
return exists
}
}
module.exports = FileManager

@ -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)) {

@ -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<any>,
}
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)
},

@ -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 = '<path/to/local/folder/for/test/chain>'
return (
<>
<div className="">
Note: To use Geth & https://remix.ethereum.org, configure it to allow requests from Remix:(see <a href="https://geth.ethereum.org/docs/rpc/server" target="_blank" rel="noreferrer">Geth Docs on rpc server</a>)
<div className="border p-1">geth --http --http.corsdomain https://remix.ethereum.org</div>
<br />
To run Remix & a local Geth test node, use this command: (see <a href="https://geth.ethereum.org/getting-started/dev-mode" target="_blank" rel="noreferrer">Geth Docs on Dev mode</a>)
<div className="border p-1">geth --http --http.corsdomain="{window.origin}" --http.api web3,eth,debug,personal,net --vmdebug --datadir {thePath} --dev console</div>
<br />
<br />
<b>WARNING:</b> It is not safe to use the --http.corsdomain flag with a wildcard: <b>--http.corsdomain *</b>
<br />
<br />For more info: <a href="https://remix-ide.readthedocs.io/en/latest/run.html#more-about-web3-provider" target="_blank" rel="noreferrer">Remix Docs on Web3 Provider</a>
<br />
<br />
External HTTP Provider Endpoint
</div>
</>
)
}
}

@ -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']
}
}

@ -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']
}
}

@ -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<string>
constructor (profile) {
super(profile)
if ((window as any).ethereum) {
this.provider = new Web3((window as any).ethereum)
}
}
sendAsync (data: JsonDataRequest): Promise<any> {
return new Promise((resolve, reject) => {
this.sendAsyncInternal(data, resolve, reject)
})
}
private async sendAsyncInternal (data: JsonDataRequest, resolve: SuccessRequest, reject: RejectRequest): Promise<void> {
// 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<string>) => {
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
}
}

@ -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)
})
}

@ -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

@ -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) {

@ -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 }

@ -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 () {

@ -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)
}

@ -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'

@ -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(/}}$/,'}'))
}

@ -50,6 +50,14 @@ const forks = {
{
number: 12965000,
name: 'london'
},
{
number: 13773000,
name: 'arrowGlacier'
},
{
number: 15050000,
name: 'grayGlacier'
}
],
3: [

@ -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

@ -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}
<input type={props.modalType === ModalTypes.password ? 'password' : 'text'} defaultValue={defaultValue} data-id="modalDialogCustomPromp" ref={ref} className="form-control" /></>
)
<input onChange={onInputChanged} type={props.modalType === ModalTypes.password ? 'password' : 'text'} defaultValue={defaultValue} data-id="modalDialogCustomPromp" ref={ref} className="form-control" />
{!validation.valid && <span className='text-warning'>{validation.message}</span>}
</>
)
}
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 (
<ModalDialog id={props.id} {...state} handleHide={props.handleHide} />
<ModalDialog id={props.id} {...state} handleHide={handleHide} />
)
}
export default ModalWrapper

@ -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 }
})
})
}

@ -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

@ -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,

@ -8,6 +8,7 @@ export const ModalInitialState: ModalState = {
hide: true,
title: '',
message: '',
validationFn: () => { return {valid: true, message: ''} },
okLabel: '',
okFn: () => { },
cancelLabel: '',

@ -331,7 +331,7 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
return { ...prevState, opt: { ...prevState.opt, debugWithGeneratedSources: checked } }
})
}} type="checkbox" title="Debug with generated sources" />
<label data-id="debugGeneratedSourcesLabel" className="form-check-label custom-control-label" htmlFor="debugGeneratedSourcesInput">Use generated sources (from Solidity v0.7.2)</label>
<label data-id="debugGeneratedSourcesLabel" className="form-check-label custom-control-label" htmlFor="debugGeneratedSourcesInput">Use generated sources (Solidity {'>='} v0.7.2)</label>
</div>
{ state.isLocalNodeUsed && <div className="mt-2 mb-2 debuggerConfig custom-control custom-checkbox">
<input className="custom-control-input" id="debugWithLocalNodeInput" onChange={({ target: { checked } }) => {

@ -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) {

@ -1,3 +1,4 @@
export * from './lib/remix-ui-helper'
export * from './lib/helper-components'
export * from './lib/components/PluginViewWrapper'
export * from './lib/components/PluginViewWrapper'
export * from './lib/components/custom-dropdown'

@ -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<HTMLButtonElement>) => (
<button
ref={ref}
onClick={(e) => {
e.preventDefault()
onClick(e)
}}
className={className.replace('dropdown-toggle', '')}
>
<div className="d-flex">
<div className="mr-auto">{ children }</div>
{ icon && <div className="pr-1"><i className={`${icon} pr-1`}></i></div> }
<div><i className="fad fa-sort-circle"></i></div>
</div>
</button>
))
// 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<HTMLDivElement>) => {
return (
<div
ref={ref}
style={style}
className={className}
aria-labelledby={labeledBy}
>
<ul className="list-unstyled mb-0">
{
children
}
</ul>
</div>
)
},
)

@ -24,10 +24,10 @@ export function Web3ProviderDialog (props: web3ProviderDialogProps) {
<br />
<b>WARNING:</b> It is not safe to use the --http.corsdomain flag with a wildcard: <b>--http.corsdomain *</b>
<br />
<br />For more info: <a href="https://remix-ide.readthedocs.io/en/latest/run.html#more-about-web3-provider" target="_blank" rel="noreferrer">Remix Docs on Web3 Provider</a>
<br />For more info: <a href="https://remix-ide.readthedocs.io/en/latest/run.html#more-about-web3-provider" target="_blank" rel="noreferrer">Remix Docs on Remix Provider</a>
<br />
<br />
Web3 Provider Endpoint
External HTTP Provider Endpoint
</div>
<input
onInput={handleInputEndpoint}

@ -1,5 +1,4 @@
import React from 'react'
import { Web3ProviderDialog } from './components/web3Dialog'
export const fileChangedToastMsg = (from: string, path: string) => (
<div><i className="fas fa-exclamation-triangle text-danger mr-1"></i>
@ -54,10 +53,6 @@ export const sourceVerificationNotAvailableToastMsg = () => (
</div>
)
export const web3Dialog = (externalEndpoint: string, setWeb3Endpoint: (value: string) => void) => (
<Web3ProviderDialog externalEndpoint={externalEndpoint} setWeb3Endpoint={setWeb3Endpoint} />
)
export const envChangeNotification = (env: { context: string, fork: string }, from: string) => (
<div>
<i className="fas fa-exclamation-triangle text-danger mr-1"></i>

@ -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]

@ -96,18 +96,20 @@ export const ModalDialog = (props: ModalDialogProps) => {
</div>
<div className="modal-footer" data-id={`${props.id}ModalDialogModalFooter-react`}>
{/* todo add autofocus ^^ */}
{ props.okLabel && <span
{ props.okLabel && <button
data-id={`${props.id}-modal-footer-ok-react`}
className={'modal-ok btn btn-sm ' + (state.toggleBtn ? 'btn-dark' : 'btn-light')}
disabled={props.validation && !props.validation.valid}
onClick={() => {
if (props.validation && !props.validation.valid) return
if (props.okFn) props.okFn()
handleHide()
}}
>
{props.okLabel ? props.okLabel : 'OK'}
</span>
</button>
}
{ props.cancelLabel && <span
{ props.cancelLabel && <button
data-id={`${props.id}-modal-footer-cancel-react`}
className={'modal-cancel btn btn-sm ' + (state.toggleBtn ? 'btn-light' : 'btn-dark')}
data-dismiss="modal"
@ -117,7 +119,7 @@ export const ModalDialog = (props: ModalDialogProps) => {
}}
>
{props.cancelLabel ? props.cancelLabel : 'Cancel'}
</span>
</button>
}
</div>
</div>

@ -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,

@ -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<any>, 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) })
}

@ -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)

@ -36,12 +36,13 @@ export const storeScenario = async (plugin: RunTab, dispatch: React.Dispatch<any
)
}
const runScenario = (plugin: RunTab, dispatch: React.Dispatch<any>, file: string, gasEstimationPrompt: (msg: string) => JSX.Element, passphrasePrompt: (msg: string) => JSX.Element, confirmDialogContent: MainnetPrompt) => {
const runScenario = (liveMode: boolean, plugin: RunTab, dispatch: React.Dispatch<any>, 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<any>, file: string
}).catch((error) => dispatch(displayNotification('Alert', error, 'OK', null)))
}
export const runCurrentScenario = (plugin: RunTab, dispatch: React.Dispatch<any>, gasEstimationPrompt: (msg: string) => JSX.Element, passphrasePrompt: (msg: string) => JSX.Element, confirmDialogContent: MainnetPrompt) => {
export const runCurrentScenario = (liveMode: boolean, plugin: RunTab, dispatch: React.Dispatch<any>, 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)
}

@ -160,8 +160,8 @@ export function AccountUI (props: AccountProps) {
accounts.map((value, index) => <option value={value} key={index}>{ loadedAccounts[value] }</option>)
}
</select>
<div style={{ marginLeft: -5 }}><CopyToClipboard content={selectedAccount} direction='top' /></div>
<i id="remixRunSignMsg" data-id="settingsRemixRunSignMsg" className="mx-1 fas fa-edit udapp_icon" aria-hidden="true" onClick={signMessage} title="Sign a message using this account key"></i>
<div style={{ marginLeft: -5 }}><CopyToClipboard tip='Copy account to clipboard' content={selectedAccount} direction='top' /></div>
<i id="remixRunSignMsg" data-id="settingsRemixRunSignMsg" className="mx-1 fas fa-edit udapp_icon" aria-hidden="true" onClick={signMessage} title="Sign a message using this account"></i>
</div>
</div>
)

@ -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) {
<div className="udapp_container" data-id="contractDropdownContainer">
<label className="udapp_settingsLabel">Contract</label>
<div className="udapp_subcontainer">
<select ref={contractsRef} value={currentContract} onChange={handleContractChange} className="udapp_contractNames custom-select" disabled={contractOptions.disabled} title={contractOptions.title} style={{ display: loadType === 'abi' ? 'none' : 'block' }}>
<select ref={contractsRef} value={currentContract} onChange={handleContractChange} className="udapp_contractNames custom-select" disabled={contractOptions.disabled} title={contractOptions.title} style={{ display: loadType === 'abi' && !isContractFile(currentFile) ? 'none' : 'block' }}>
{ (contractList[currentFile] || []).map((contract, index) => {
return <option key={index} value={contract.alias}>{contract.alias} - {contract.file}</option>
}) }
@ -258,7 +262,7 @@ export function ContractDropdownUI (props: ContractDropdownProps) {
</div> : ''
}
</div>
<div className="udapp_orLabel mt-2" style={{ display: loadType === 'abi' ? 'none' : 'block' }}>or</div>
<div className="udapp_orLabel mt-2" style={{ display: loadType === 'abi' && !isContractFile(currentFile) ? 'none' : 'block' }}>or</div>
<div className="udapp_button udapp_atAddressSect">
<button className="udapp_atAddress btn btn-sm btn-info" id="runAndDeployAtAdressButton" disabled={atAddressOptions.disabled} title={atAddressOptions.title} onClick={loadFromAddress}>At Address</button>
<input

@ -1,35 +1,73 @@
// eslint-disable-next-line no-use-before-define
import React from 'react'
import { EnvironmentProps } from '../types'
import { Dropdown } from 'react-bootstrap'
import { CustomMenu, CustomToggle } from '@remix-ui/helper'
import { OverlayTrigger, Tooltip } from 'react-bootstrap' // eslint-disable-line
export function EnvironmentUI (props: EnvironmentProps) {
const handleChangeExEnv = (env: string) => {
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 (
<div className="udapp_crow">
<label id="selectExEnv" className="udapp_settingsLabel">
Environment
Environment <OverlayTrigger placement={'right'} overlay={
<Tooltip className="text-nowrap" id="info-recorder">
<span>Open chainlist and add a new provider for the chain you want to interact to.</span>
</Tooltip>
}>
<a href='https://chainlist.org/' target='_blank'><i style={{ fontSize: 'medium' }} className={'ml-2 fad fa-plug'} aria-hidden="true"></i></a>
</OverlayTrigger>
</label>
<div className="udapp_environment">
<select id="selectExEnvOptions" data-id="settingsSelectEnvOptions" className="form-control udapp_select custom-select" value={props.selectedEnv || ''} onChange={(e) => { handleChangeExEnv(e.target.value) }}>
{
props.providers.providerList.map((provider, index) =>
<option id={provider.id} key={index} data-id={provider.dataId}
title={provider.title}
value={provider.value}> { provider.content }
</option>
)
}
</select>
<a href="https://remix-ide.readthedocs.io/en/latest/run.html#run-setup" target="_blank" rel="noreferrer"><i className="udapp_infoDeployAction ml-2 fas fa-info" title="check out docs to setup Environment"></i></a>
<Dropdown id="selectExEnvOptions" data-id="settingsSelectEnvOptions" className='udapp_selectExEnvOptions'>
<Dropdown.Toggle as={CustomToggle} id="dropdown-custom-components" className="btn btn-light btn-block w-100 d-inline-block border border-dark form-control" icon={null}>
{ isL2(currentProvider) && 'L2 - '}
{ currentProvider && currentProvider.content }
{ currentProvider && bridges[currentProvider.value] && <OverlayTrigger placement={'right'} overlay={
<Tooltip className="text-nowrap" id="info-recorder">
<span>Click to open a bridge for converting L1 mainnet ETH to the selected network currency.</span>
</Tooltip>
}>
<i style={{ fontSize: 'medium' }} className={'ml-2 fal fa-plug'} aria-hidden="true" onClick={() => { window.open(bridges[currentProvider.value], '_blank') }}></i>
</OverlayTrigger>}
</Dropdown.Toggle>
<Dropdown.Menu as={CustomMenu} className='w-100 custom-dropdown-items' data-id="custom-dropdown-items" >
{
props.providers.providerList.map(({ content, value }, index) => (
<Dropdown.Item
key={index}
onClick={() => {
handleChangeExEnv(value)
}}
data-id={`dropdown-item-${value}`}
>
<span className="pl-3">{ isL2({ value }) && 'L2 - ' }{ content }</span>
</Dropdown.Item>
))
}
</Dropdown.Menu>
</Dropdown>
<a href="https://remix-ide.readthedocs.io/en/latest/run.html#environment" target="_blank" rel="noreferrer"><i className="udapp_infoDeployAction ml-2 fas fa-info" title="Click for docs about Environment"></i></a>
</div>
</div>
)

@ -10,7 +10,7 @@ export function GasPriceUI (props: GasPriceProps) {
return (
<div className="udapp_crow">
<label className="udapp_settingsLabel">Gas limit</label>
<input type="number" className="form-control udapp_gasNval udapp_col2" id="gasLimit" value={props.gasLimit} onChange={handleGasLimit} />
<input type="number" className="form-control udapp_gasNval udapp_col2" title="The default gas limit is 3M. Adjust as needed." id="gasLimit" value={props.gasLimit} onChange={handleGasLimit} />
</div>
)
}

@ -12,7 +12,7 @@ export function InstanceContainerUI (props: InstanceContainerProps) {
return (
<div className="udapp_instanceContainer mt-3 border-0 list-group-item">
<label className="udapp_deployedContracts d-flex justify-content-between align-items-center pl-2 mb-1"
<label className="udapp_deployedContracts d-flex justify-content-between align-items-center pl-2 mb-2"
title="Autogenerated generic user interfaces for interaction with deployed contracts">
Deployed Contracts
{ instanceList.length > 0

@ -1,15 +1,18 @@
// eslint-disable-next-line no-use-before-define
import React, {useState} from 'react'
import React, {useRef, useState} from 'react'
import { RecorderProps } from '../types'
import { OverlayTrigger, Tooltip } from 'react-bootstrap' // eslint-disable-line
export function RecorderUI (props: RecorderProps) {
const inputLive = useRef<HTMLInputElement>()
const [toggleExpander, setToggleExpander] = useState<boolean>(false)
const triggerRecordButton = () => {
props.storeScenario(props.scenarioPrompt)
}
const handleClickRunButton = () => {
props.runCurrentScenario(props.gasEstimationPrompt, props.passphrasePrompt, props.mainnetPrompt)
const liveMode = inputLive.current ? inputLive.current.checked : false
props.runCurrentScenario(liveMode, props.gasEstimationPrompt, props.passphrasePrompt, props.mainnetPrompt)
}
const toggleClass = () => {
@ -22,6 +25,14 @@ export function RecorderUI (props: RecorderProps) {
<div className="d-flex">
<label className="mt-1 udapp_recorderSectionLabel">Transactions recorded</label>
<div className="ml-2 mb-2 badge badge-pill badge-primary" title="The number of recorded transactions">{props.count}</div>
<OverlayTrigger placement={'right'} overlay={
<Tooltip className="text-nowrap" id="info-recorder">
<span>Save transactions (deployed contracts and function executions) and replay them in another environment. <br/> e.g Transactions created in Javascript VM can be replayed in the Injected Web3.
</span>
</Tooltip>
}>
<i style={{ fontSize: 'medium' }} className={'ml-2 fal fa-info-circle'} aria-hidden="true"></i>
</OverlayTrigger>
</div>
<div>
<span data-id='udappRecorderTitleExpander' onClick={toggleClass}>
@ -29,17 +40,29 @@ export function RecorderUI (props: RecorderProps) {
</span>
</div>
</div>
<div className={`border-bottom flex-column ${toggleExpander ? "d-flex" : "d-none"}`}>
<div className="p-2 mt-2">
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.
</div>
<div className="mb-2 udapp_transactionActions">
<i className="fas fa-save savetransaction udapp_recorder udapp_icon"
onClick={triggerRecordButton} title="Save Transactions" aria-hidden="true">
</i>
<i className="fas fa-play runtransaction udapp_runTxs udapp_icon" title="Run Transactions" data-id="runtransaction" aria-hidden="true" onClick={handleClickRunButton}></i>
</div>
<div className={`flex-column ${toggleExpander ? "d-flex" : "d-none"}`}>
<div className="mb-1 mt-1 fmt-2 custom-control custom-checkbox mb-1">
<input ref={inputLive} type="checkbox" id="livemode-recorder" className="custom-control-input custom-select" name="input-livemode"/>
<label className="form-check-label custom-control-label" data-id="runtabLivemodeInput" htmlFor="livemode-recorder">Use live mode (Run transactions against latest compiled contracts).</label>
</div>
<div className="mb-1 mt-1 udapp_transactionActions">
<OverlayTrigger placement={'right'} overlay={
<Tooltip className="text-nowrap" id="tooltip-save-recorder">
<span>Save {props.count} transaction(s) as scenario file.
</span>
</Tooltip>
}>
<button className="btn btn-sm btn-info savetransaction udapp_recorder" onClick={triggerRecordButton}>Save</button>
</OverlayTrigger>
<OverlayTrigger placement={'right'} overlay={
<Tooltip className="text-nowrap" id="tooltip-run-recorder">
<span>Run transaction(s) from the current scenario file.
</span>
</Tooltip>
}>
<button className="btn btn-sm btn-info runtransaction udapp_runTxs" data-id="runtransaction" onClick={handleClickRunButton}>Run</button>
</OverlayTrigger>
</div>
</div>
</div>
)

@ -204,7 +204,7 @@ export function UniversalDappUI (props: UdappProps) {
return (
<div className={`instance udapp_instance udapp_run-instance border-dark ${toggleExpander ? 'udapp_hidesub' : 'bg-light'}`} id={`instance${address}`} data-shared="universalDappUiInstance">
<div className="udapp_title alert alert-secondary">
<div className="udapp_title pb-0 alert alert-secondary">
<span data-id={`universalDappUiTitleExpander${props.index}`} className="btn udapp_titleExpander" onClick={toggleClass}>
<i className={`fas ${toggleExpander ? 'fa-angle-right' : 'fa-angle-down'}`} aria-hidden="true"></i>
</span>

@ -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}

@ -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%;
}

@ -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,

@ -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<any, any> {
constructor(config: any);
event: any;
@ -70,7 +73,7 @@ export class Blockchain extends Plugin<any, any> {
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<any, any> {
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";

@ -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,

@ -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";

@ -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)
}

@ -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'

@ -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<string>("")
const [githubUserName, setGithubUsername] = useState<string>("")
const [githubEmail, setGithubEmail] = useState<string>("")
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 (
<div className="border-top">
<div className="card-body pt-3 pb-2">
<h6 className="card-title">GitHub Credentials</h6>
<p className="mb-1">Manage your GitHub credentials used to publish to Gist and retrieve GitHub contents.</p>
<p className="">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.</p>
<p className="mb-1"><a className="text-primary" target="_blank" href="https://github.com/settings/tokens">https://github.com/settings/tokens</a></p>
<div>
<label>TOKEN:</label>
<div className="input-group text-secondary mb-0 h6">
<input id="gistaccesstoken" data-id="settingsTabGistAccessToken" type="password" className="form-control" onChange={(e) => handleChangeTokenState(e)} value={ githubToken } />
<div className="input-group-append">
<CopyToClipboard content={githubToken} data-id='copyToClipboardCopyIcon' className='far fa-copy ml-1 p-2 mt-1' direction={"top"} />
</div>
</div>
</div>
<div>
<label>USERNAME:</label>
<div className="text-secondary mb-0 h6">
<input id="githubusername" data-id="settingsTabGithubUsername" type="text" className="form-control" onChange={(e) => handleChangeUserNameState(e)} value={ githubUserName } />
</div>
</div>
<div>
<label>EMAIL:</label>
<div className="text-secondary mb-0 h6">
<input id="githubemail" data-id="settingsTabGithubEmail" type="text" className="form-control" onChange={(e) => handleChangeEmailState(e)} value={ githubEmail } />
<div className="d-flex justify-content-end pt-2">
<input className="btn btn-sm btn-primary ml-2" id="savegisttoken" data-id="settingsTabSaveGistToken" onClick={saveGithubToken} value="Save" type="button" disabled={githubToken === ''}></input>
<button className="btn btn-sm btn-secondary ml-2" id="removegisttoken" data-id="settingsTabRemoveGistToken" title="Delete GitHub Credentials" onClick={removeToken}>Remove</button>
</div>
</div>
</div>
</div>
</div>
)
}

@ -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) => {
<div>
{state.message ? <Toaster message= {state.message}/> : null}
{generalConfig()}
{token('gist')}
<GithubSettings
saveTokenToast={(githubToken: string, githubUserName: string, githubEmail: string) => {
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()}

@ -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) => {

@ -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
}
}

@ -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) => {
<div className={`flex-column 'd-flex'}`}>
<div className="mb-2 ml-4">
<label className="remixui_compilerLabel form-check-label" htmlFor="compilierLanguageSelector">Language</label>
<select onChange={(e) => handleLanguageChange(e.target.value)} disabled={state.useFileConfiguration} value={state.language} className="custom-select" id="compilierLanguageSelector" title="Available since v0.5.7">
<select onChange={(e) => handleLanguageChange(e.target.value)} disabled={state.useFileConfiguration} value={state.language} className="custom-select" id="compilierLanguageSelector" title="Language specification available from Compiler >= v0.5.7">
<option data-id={state.language === 'Solidity' ? 'selected' : ''} value='Solidity'>Solidity</option>
<option data-id={state.language === 'Yul' ? 'selected' : ''} value='Yul'>Yul</option>
</select>
@ -789,7 +803,7 @@ export const CompilerContainer = (props: CompilerContainerProps) => {
</div>
<div className={`pt-2 ml-4 ml-2 align-items-start justify-content-between d-flex`}>
{ (!showFilePathInput && state.useFileConfiguration) && <span
title="Click to open the config file."
title="Click to open the config file"
onClick={configFilePath === '' ? () => {} : openFile}
className="py-2 remixui_compilerConfigPath"
>{configFilePath === '' ? 'No file selected.' : configFilePath}</span> }
@ -861,7 +875,7 @@ export const CompilerContainer = (props: CompilerContainerProps) => {
}>
<a href="https://remix-ide.readthedocs.io/en/latest/running_js_scripts.html#compile-a-contract-and-run-a-script-on-the-fly" target="_blank" ><i className="pl-2 ml-2 mt-3 mb-1 fas fa-info text-dark"></i></a>
</OverlayTrigger>
<CopyToClipboard tip="Copy tag to use in contract NatSpec" getContent={() => '@custom:dev-run-script file_path'} direction='top'>
<CopyToClipboard tip="Click to copy the custom NatSpec tag" getContent={() => '@custom:dev-run-script file_path'} direction='top'>
<button className="btn remixui_copyButton ml-2 mt-3 mb-1 text-dark">
<i className="remixui_copyIcon far fa-copy" aria-hidden="true"></i>
</button>

@ -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') {

@ -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}

@ -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,

@ -677,7 +677,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => { // eslint-d
<button
className="btn border w-50"
data-id="testTabGenerateTestFile"
title="Generate sample test file."
title="Generate a sample test file"
disabled={disableGenerateButton}
onClick={async () => {
await testTabLogic.generateTestFile((err:any) => { if (err) setToasterMsg(err)}) // eslint-disable-line @typescript-eslint/no-explicit-any

@ -15,7 +15,7 @@ const RenderCall = ({ tx, resolvedData, logs, index, plugin, showTableHash, txDe
const debug = (event, tx) => {
event.stopPropagation()
if (tx.isCall && tx.envMode !== 'vm') {
modal('VM mode', 'Cannot debug this call. Debugging calls is only possible in JavaScript VM mode.', 'Ok', true, () => {}, 'Cancel', () => {})
modal('VM mode', 'Cannot debug this call. Debugging calls is only possible in Remix VM mode.', 'Ok', true, () => {}, 'Cancel', () => {})
} else {
plugin.event.trigger('debuggingRequested', [tx.hash])
}

@ -10,7 +10,7 @@ const RenderKnownTransactions = ({ tx, receipt, resolvedData, logs, index, plugi
const debug = (event, tx) => {
event.stopPropagation()
if (tx.isCall && tx.envMode !== 'vm') {
modal('VM mode', 'Cannot debug this call. Debugging calls is only possible in JavaScript VM mode.', 'Ok', true, () => {}, 'Cancel', () => {})
modal('VM mode', 'Cannot debug this call. Debugging calls is only possible in Remix VM mode.', 'Ok', true, () => {}, 'Cancel', () => {})
} else {
plugin.event.trigger('debuggingRequested', [tx.hash])
}

@ -7,7 +7,7 @@ const RenderUnKnownTransactions = ({ tx, receipt, index, plugin, showTableHash,
const debug = (event, tx) => {
event.stopPropagation()
if (tx.isCall && tx.envMode !== 'vm') {
modal('VM mode', 'Cannot debug this call. Debugging calls is only possible in JavaScript VM mode.', 'Ok', true, () => {}, 'Cancel', () => {})
modal('VM mode', 'Cannot debug this call. Debugging calls is only possible in Remix VM mode.', 'Ok', true, () => {}, 'Cancel', () => {})
} else {
plugin.event.trigger('debuggingRequested', [tx.hash])
}

@ -0,0 +1,12 @@
{
"presets": [
[
"@nrwl/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}

@ -0,0 +1,18 @@
{
"extends": ["plugin:@nrwl/nx/react", "../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

@ -0,0 +1,7 @@
# remix-ui-tooltip-popup
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test remix-ui-tooltip-popup` to execute the unit tests via [Jest](https://jestjs.io).

@ -0,0 +1 @@
export * from './lib/tooltip-popup'

@ -0,0 +1,27 @@
import React, { useState } from 'react'
import { OverlayTrigger, Popover } from 'react-bootstrap'
import { TooltipPopupProps } from '../types'
import './tooltip-popup.module.css'
const popover = (title?: string, content?: string | React.ReactNode) => (
<Popover id="popover-basic" className='bg-light border-secondary'>
<Popover.Title as="h3" className='bg-dark border-0'>{ title || 'Tooltip' }</Popover.Title>
<Popover.Content>
{ content }
</Popover.Content>
</Popover>
)
export function TooltipPopup(props: TooltipPopupProps) {
const [show, setShow] = useState<boolean>(false)
return (
<OverlayTrigger trigger="click" placement={"bottom"} overlay={popover(props.title, props.children || props.content)} show={show} onToggle={(nextShow) => {
setShow(nextShow)
}}>
<i className={`${props.icon} remixui_menuicon pr-0 mr-2`}></i>
</OverlayTrigger>
)
}
export default TooltipPopup

@ -0,0 +1,6 @@
export interface TooltipPopupProps {
children?: React.ReactNode,
title?: string,
content?: string,
icon: string
}

@ -0,0 +1,20 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"types": ["node"]
},
"files": [
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": ["**/*.spec.ts", "**/*.spec.tsx"],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

@ -6,7 +6,9 @@ import { displayNotification, displayPopUp, fetchDirectoryError, fetchDirectoryR
import { listenOnPluginEvents, listenOnProviderEvents } from './events'
import { createWorkspaceTemplate, getWorkspaces, loadWorkspacePreset, setPlugin } from './workspace'
import { QueryParams } from '@remix-project/remix-lib'
import { fetchContractFromEtherscan } from '@remix-project/core-plugin' // eslint-disable-line
import JSZip from 'jszip'
import axios, { AxiosResponse } from 'axios'
export * from './events'
export * from './workspace'
@ -22,6 +24,24 @@ export type UrlParametersType = {
url: string
}
const basicWorkspaceInit = async (workspaces: { name: string; isGitRepo: boolean; }[], workspaceProvider) => {
if (workspaces.length === 0) {
await createWorkspaceTemplate('default_workspace', 'remixDefault')
plugin.setWorkspace({ name: 'default_workspace', isLocalhost: false })
dispatch(setCurrentWorkspace({ name: 'default_workspace', isGitRepo: false }))
await loadWorkspacePreset('remixDefault')
} else {
if (workspaces.length > 0) {
const workspace = workspaces[workspaces.length - 1]
const workspaceName = (workspace || {}).name
workspaceProvider.setWorkspace(workspaceName)
plugin.setWorkspace({ name: workspaceName, isLocalhost: false })
dispatch(setCurrentWorkspace(workspace))
}
}
}
export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.Dispatch<any>) => {
if (filePanelPlugin) {
plugin = filePanelPlugin
@ -31,32 +51,84 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
const localhostProvider = filePanelPlugin.fileProviders.localhost
const params = queryParams.get() as UrlParametersType
const workspaces = await getWorkspaces() || []
dispatch(setWorkspaces(workspaces))
if (params.gist) {
await createWorkspaceTemplate('gist-sample', 'gist-template')
plugin.setWorkspace({ name: 'gist-sample', isLocalhost: false })
dispatch(setCurrentWorkspace('gist-sample'))
dispatch(setCurrentWorkspace({ name: 'gist-sample', isGitRepo: false }))
await loadWorkspacePreset('gist-template')
} else if (params.code || params.url) {
await createWorkspaceTemplate('code-sample', 'code-template')
plugin.setWorkspace({ name: 'code-sample', isLocalhost: false })
dispatch(setCurrentWorkspace('code-sample'))
dispatch(setCurrentWorkspace({ name: 'code-sample', isGitRepo: false }))
const filePath = await loadWorkspacePreset('code-template')
plugin.on('editor', 'editorMounted', async () => await plugin.fileManager.openFile(filePath))
} else {
if (workspaces.length === 0) {
await createWorkspaceTemplate('default_workspace', 'remixDefault')
plugin.setWorkspace({ name: 'default_workspace', isLocalhost: false })
dispatch(setCurrentWorkspace('default_workspace'))
await loadWorkspacePreset('remixDefault')
} else {
if (workspaces.length > 0) {
workspaceProvider.setWorkspace(workspaces[workspaces.length - 1])
plugin.setWorkspace({ name: workspaces[workspaces.length - 1], isLocalhost: false })
dispatch(setCurrentWorkspace(workspaces[workspaces.length - 1]))
} else if (window.location.pathname && window.location.pathname !== '/') {
let route = window.location.pathname
if (route.startsWith('/address/0x') && route.length === 51) {
const contractAddress = route.split('/')[2]
plugin.call('notification', 'toast', `Looking for contract(s) verified on different networks of Etherscan for contract address ${contractAddress} .....`)
let data
let count = 0
try {
let etherscanKey = await plugin.call('config', 'getAppParameter', 'etherscan-access-token')
if (!etherscanKey) etherscanKey = '2HKUX5ZVASZIKWJM8MIQVCRUVZ6JAWT531'
const networks = [
{id: 1, name: 'mainnet'},
{id: 3, name: 'ropsten'},
{id: 4, name: 'rinkeby'},
{id: 42, name: 'kovan'},
{id: 5, name: 'goerli'}
]
let found = false
const foundOnNetworks = []
for (const network of networks) {
const target = `/${network.name}/${contractAddress}`
try {
data = await fetchContractFromEtherscan(plugin, network, contractAddress, target, etherscanKey)
} catch (error) {
if ((error.message.startsWith('contract not verified on Etherscan') || error.message.startsWith('unable to retrieve contract data')) && network.id !== 5)
continue
else {
if (!found) await basicWorkspaceInit(workspaces, workspaceProvider)
break
}
}
found = true
foundOnNetworks.push(network.name)
await createWorkspaceTemplate('etherscan-code-sample', 'code-template')
plugin.setWorkspace({ name: 'etherscan-code-sample', isLocalhost: false })
dispatch(setCurrentWorkspace({ name: 'etherscan-code-sample', isGitRepo: false }))
let filePath
count = count + (Object.keys(data.compilationTargets)).length
for (filePath in data.compilationTargets)
await workspaceProvider.set(filePath, data.compilationTargets[filePath]['content'])
plugin.on('editor', 'editorMounted', async () => await plugin.fileManager.openFile(filePath))
}
plugin.call('notification', 'toast', `Added ${count} verified contract${count === 1 ? '': 's'} from ${foundOnNetworks.join(',')} network${foundOnNetworks.length === 1 ? '': 's'} of Etherscan for contract address ${contractAddress} !!`)
} catch (error) {
await basicWorkspaceInit(workspaces, workspaceProvider)
}
}
} else if (route.endsWith('.sol')) {
if (route.includes('blob')) route = route.replace('/blob', '')
let response: AxiosResponse
try {
response = await axios.get(`https://raw.githubusercontent.com${route}`)
} catch (error) {
plugin.call('notification', 'toast', `cound not find ${route} on GitHub`)
await basicWorkspaceInit(workspaces, workspaceProvider)
}
if (response && response.status === 200) {
const content = response.data
await createWorkspaceTemplate('github-code-sample', 'code-template')
plugin.setWorkspace({ name: 'github-code-sample', isLocalhost: false })
dispatch(setCurrentWorkspace({ name: 'github-code-sample', isGitRepo: false }))
await workspaceProvider.set(route, content)
plugin.on('editor', 'editorMounted', async () => await plugin.fileManager.openFile(route))
} else await basicWorkspaceInit(workspaces, workspaceProvider)
} else await basicWorkspaceInit(workspaces, workspaceProvider)
} else {
await basicWorkspaceInit(workspaces, workspaceProvider)
}
listenOnPluginEvents(plugin)

@ -1,13 +1,13 @@
import { action } from '../types'
export const setCurrentWorkspace = (workspace: string) => {
export const setCurrentWorkspace = (workspace: { name: string; isGitRepo: boolean; }) => {
return {
type: 'SET_CURRENT_WORKSPACE',
payload: workspace
}
}
export const setWorkspaces = (workspaces: string[]) => {
export const setWorkspaces = (workspaces: { name: string; isGitRepo: boolean; }[]) => {
return {
type: 'SET_WORKSPACES',
payload: workspaces
@ -125,7 +125,7 @@ export const createWorkspaceRequest = (promise: Promise<any>) => {
}
}
export const createWorkspaceSuccess = (workspaceName: string) => {
export const createWorkspaceSuccess = (workspaceName: { name: string; isGitRepo: boolean; }) => {
return {
type: 'CREATE_WORKSPACE_SUCCESS',
payload: workspaceName
@ -239,3 +239,21 @@ export const fsInitializationCompleted = () => {
type: 'FS_INITIALIZATION_COMPLETED'
}
}
export const cloneRepositoryRequest = () => {
return {
type: 'CLONE_REPOSITORY_REQUEST'
}
}
export const cloneRepositorySuccess = () => {
return {
type: 'CLONE_REPOSITORY_SUCCESS'
}
}
export const cloneRepositoryFailed = () => {
return {
type: 'CLONE_REPOSITORY_FAILED'
}
}

@ -1,8 +1,8 @@
import React from 'react'
import { bufferToHex, keccakFromString } from 'ethereumjs-util'
import axios, { AxiosResponse } from 'axios'
import { addInputFieldSuccess, createWorkspaceError, createWorkspaceRequest, createWorkspaceSuccess, displayNotification, fetchWorkspaceDirectoryError, fetchWorkspaceDirectoryRequest, fetchWorkspaceDirectorySuccess, hideNotification, setCurrentWorkspace, setDeleteWorkspace, setMode, setReadOnlyMode, setRenameWorkspace } from './payload'
import { checkSlash, checkSpecialChars } from '@remix-ui/helper'
import { addInputFieldSuccess, cloneRepositoryFailed, cloneRepositoryRequest, cloneRepositorySuccess, createWorkspaceError, createWorkspaceRequest, createWorkspaceSuccess, displayNotification, displayPopUp, fetchWorkspaceDirectoryError, fetchWorkspaceDirectoryRequest, fetchWorkspaceDirectorySuccess, hideNotification, setCurrentWorkspace, setDeleteWorkspace, setMode, setReadOnlyMode, setRenameWorkspace } from './payload'
import { checkSlash, checkSpecialChars, createNonClashingTitle } from '@remix-ui/helper'
import { JSONStandardInput, WorkspaceTemplate } from '../types'
import { QueryParams } from '@remix-project/remix-lib'
@ -42,13 +42,13 @@ export const addInputField = async (type: 'file' | 'folder', path: string, cb?:
return promise
}
export const createWorkspace = async (workspaceName: string, workspaceTemplateName: WorkspaceTemplate, isEmpty = false, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
export const createWorkspace = async (workspaceName: string, workspaceTemplateName: WorkspaceTemplate, isEmpty = false, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void, isGitRepo: boolean = false) => {
await plugin.fileManager.closeAllFiles()
const promise = createWorkspaceTemplate(workspaceName, workspaceTemplateName)
dispatch(createWorkspaceRequest(promise))
promise.then(async () => {
dispatch(createWorkspaceSuccess(workspaceName))
dispatch(createWorkspaceSuccess({ name: workspaceName, isGitRepo }))
await plugin.setWorkspace({ name: workspaceName, isLocalhost: false })
await plugin.setWorkspaces(await getWorkspaces())
await plugin.workspaceCreated(workspaceName)
@ -254,8 +254,10 @@ export const switchToWorkspace = async (name: string) => {
if (isActive) await plugin.call('manager', 'deactivatePlugin', 'remixd')
await plugin.fileProviders.workspace.setWorkspace(name)
await plugin.setWorkspace({ name, isLocalhost: false })
const isGitRepo = await plugin.fileManager.isGitRepo()
dispatch(setMode('browser'))
dispatch(setCurrentWorkspace(name))
dispatch(setCurrentWorkspace({ name, isGitRepo }))
dispatch(setReadOnlyMode(false))
}
}
@ -302,22 +304,69 @@ export const uploadFile = async (target, targetFolder: string, cb?: (err: Error,
})
}
export const getWorkspaces = async (): Promise<string[]> | undefined => {
export const getWorkspaces = async (): Promise<{name: string, isGitRepo: boolean}[]> | undefined => {
try {
const workspaces: string[] = await new Promise((resolve, reject) => {
const workspaces: {name: string, isGitRepo: boolean}[] = await new Promise((resolve, reject) => {
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
plugin.fileProviders.browser.resolveDirectory('/' + workspacesPath, (error, items) => {
if (error) {
return reject(error)
}
resolve(Object.keys(items)
Promise.all(Object.keys(items)
.filter((item) => items[item].isDirectory)
.map((folder) => folder.replace(workspacesPath + '/', '')))
.map(async (folder) => {
const isGitRepo: boolean = await plugin.fileProviders.browser.exists('/' + folder + '/.git')
return {
name: folder.replace(workspacesPath + '/', ''),
isGitRepo
}
})).then(workspacesList => resolve(workspacesList))
})
})
await plugin.setWorkspaces(workspaces)
return workspaces
} catch (e) {}
} catch (e) {}
}
export const cloneRepository = async (url: string) => {
const config = plugin.registry.get('config').api
const token = config.get('settings/gist-access-token')
const repoConfig = { url, token }
const urlArray = url.split('/')
let repoName = urlArray.length > 0 ? urlArray[urlArray.length - 1] : ''
try {
repoName = await createNonClashingTitle(repoName, plugin.fileManager)
await createWorkspace(repoName, 'blank', true, null, true)
const promise = plugin.call('dGitProvider', 'clone', repoConfig, repoName, true)
dispatch(cloneRepositoryRequest())
promise.then(async () => {
const isActive = await plugin.call('manager', 'isActive', 'dgit')
if (!isActive) await plugin.call('manager', 'activatePlugin', 'dgit')
await fetchWorkspaceDirectory(repoName)
dispatch(cloneRepositorySuccess())
}).catch((e) => {
const cloneModal = {
id: 'cloneGitRepository',
title: 'Clone Git Repository',
message: 'An error occured: ' + e,
modalType: 'modal',
okLabel: 'OK',
okFn: async () => {
await deleteWorkspace(repoName)
dispatch(cloneRepositoryFailed())
},
hideFn: async () => {
await deleteWorkspace(repoName)
dispatch(cloneRepositoryFailed())
}
}
plugin.call('notification', 'modal', cloneModal)
})
} catch (e) {
dispatch(displayPopUp('An error occured: ' + e))
}
}

@ -4,7 +4,7 @@ import { BrowserState } from '../reducers/workspace'
export const FileSystemContext = createContext<{
fs: BrowserState,
modal:(title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => void,
modal:(title: string | JSX.Element, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => void,
dispatchInitWorkspace:() => Promise<void>,
dispatchFetchDirectory:(path: string) => Promise<void>,
dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>,
@ -29,5 +29,6 @@ export const FileSystemContext = createContext<{
dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise<void>
dispatchHandleExpandPath: (paths: string[]) => Promise<void>,
dispatchHandleDownloadFiles: () => Promise<void>,
dispatchHandleRestoreBackup: () => Promise<void>
dispatchHandleRestoreBackup: () => Promise<void>,
dispatchCloneRepository: (url: string) => Promise<void>
}>(null)

@ -62,4 +62,42 @@
.remixui_menuicon:hover {
transform: scale(1.3);
}
.remixui_cloneContainer {
display: flex;
align-items: center;
height: 32px;
}
.remixui_cloneContainer input {
height: 32px;
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
width: 250px;
font-size: 10px !important;
padding: .25rem;
}
.remixui_menuicon .bs-popover-auto[x-placement^="bottom"] .popover-header::before, .bs-popover-bottom .popover-header::before {
border-bottom-color: var(--dark) !important
}
.remixui_menuicon .bs-popover-auto[x-placement^="bottom"] > .arrow::after, .bs-popover-bottom > .arrow::after {
border-bottom-color: var(--dark) !important
}
.custom-dropdown-items {
padding: 0.25rem 0.25rem;
border-radius: .25rem;
background: var(--light);
}
.custom-dropdown-items a {
border-radius: .25rem;
text-transform: none;
text-decoration: none;
font-weight: normal;
font-size: 0.875rem;
padding: 0.25rem 0.25rem;
width: auto;
color: var(--text);
}

@ -5,7 +5,7 @@ import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { FileSystemContext } from '../contexts'
import { browserReducer, browserInitialState } from '../reducers/workspace'
import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder, deletePath, renamePath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile, handleDownloadFiles, restoreBackupZip } from '../actions'
import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder, deletePath, renamePath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile, handleDownloadFiles, restoreBackupZip, cloneRepository } from '../actions'
import { Modal, WorkspaceProps, WorkspaceTemplate } from '../types'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Workspace } from '../remix-ui-workspace'
@ -123,6 +123,10 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
await restoreBackupZip()
}
const dispatchCloneRepository = async (url: string) => {
await cloneRepository(url)
}
useEffect(() => {
dispatchInitWorkspace()
}, [])
@ -224,7 +228,8 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
dispatchHandleClickFile,
dispatchHandleExpandPath,
dispatchHandleDownloadFiles,
dispatchHandleRestoreBackup
dispatchHandleRestoreBackup,
dispatchCloneRepository
}
return (
<FileSystemContext.Provider value={value}>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save