Merge branch 'master' of https://github.com/ethereum/remix-project into editorcontext

pull/5370/head
filip mertens 2 years ago
commit 2d4be91941
  1. 6
      .circleci/config.yml
  2. 31
      README.md
  3. 16
      apps/remix-ide-e2e/src/commands/createContract.ts
  4. 14
      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. 10
      apps/remix-ide-e2e/src/tests/ballot.test.ts
  8. 2
      apps/remix-ide-e2e/src/tests/ballot_0_4_11.test.ts
  9. 18
      apps/remix-ide-e2e/src/tests/debugger.test.ts
  10. 4
      apps/remix-ide-e2e/src/tests/generalSettings.test.ts
  11. 8
      apps/remix-ide-e2e/src/tests/libraryDeployment.test.ts
  12. 4
      apps/remix-ide-e2e/src/tests/migrateFileSystem.test.ts
  13. 12
      apps/remix-ide-e2e/src/tests/plugin_api.ts
  14. 8
      apps/remix-ide-e2e/src/tests/providers.test.ts
  15. 16
      apps/remix-ide-e2e/src/tests/recorder.test.ts
  16. 2
      apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts
  17. 2
      apps/remix-ide-e2e/src/tests/signingMessage.test.ts
  18. 2
      apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts
  19. 12
      apps/remix-ide-e2e/src/tests/specialFunctions.test.ts
  20. 8
      apps/remix-ide-e2e/src/tests/terminal.test.ts
  21. 6
      apps/remix-ide-e2e/src/tests/transactionExecution.test.ts
  22. 4
      apps/remix-ide-e2e/src/tests/url.test.ts
  23. 14
      apps/remix-ide-e2e/src/tests/workspace.test.ts
  24. 6
      apps/remix-ide-e2e/src/types/index.d.ts
  25. 2
      apps/remix-ide/ci/deploy_from_travis_remix-alpha.sh
  26. 2
      apps/remix-ide/ci/deploy_from_travis_remix-beta.sh
  27. 2
      apps/remix-ide/ci/deploy_from_travis_remix-live.sh
  28. 15
      apps/remix-ide/src/404.html
  29. 6
      apps/remix-ide/src/app.js
  30. 7
      apps/remix-ide/src/app/files/dgitProvider.js
  31. 9
      apps/remix-ide/src/app/files/fileManager.ts
  32. 3
      apps/remix-ide/src/app/panels/file-panel.js
  33. 8
      apps/remix-ide/src/app/tabs/abstract-provider.tsx
  34. 2
      apps/remix-ide/src/app/tabs/external-http-provider.tsx
  35. 21
      apps/remix-ide/src/app/tabs/injected-arbitrum-one-provider.tsx
  36. 21
      apps/remix-ide/src/app/tabs/injected-optimism-provider.tsx
  37. 75
      apps/remix-ide/src/app/tabs/injected-provider.tsx
  38. 34
      apps/remix-ide/src/app/udapp/run-tab.js
  39. 53
      apps/remix-ide/src/blockchain/blockchain.js
  40. 15
      apps/remix-ide/src/blockchain/execution-context.js
  41. 2
      apps/remix-ide/src/remixAppManager.js
  42. 14
      libs/remix-core-plugin/src/lib/constants/uups.ts
  43. 50
      libs/remix-core-plugin/src/lib/openzeppelin-proxy.ts
  44. 133
      libs/remix-core-plugin/src/types/contract.ts
  45. 1
      libs/remix-ui/helper/src/index.ts
  46. 42
      libs/remix-ui/helper/src/lib/components/custom-dropdown.tsx
  47. 18
      libs/remix-ui/helper/src/lib/helper-components.tsx
  48. 16
      libs/remix-ui/helper/src/lib/remix-ui-helper.ts
  49. 46
      libs/remix-ui/panel/src/lib/plugins/panel-header.tsx
  50. 1
      libs/remix-ui/panel/src/lib/plugins/panel.css
  51. 3
      libs/remix-ui/plugin-manager/src/lib/components/ActivePluginCard.tsx
  52. 3
      libs/remix-ui/plugin-manager/src/lib/components/InactivePluginCard.tsx
  53. 6
      libs/remix-ui/plugin-manager/src/lib/components/LocalPluginForm.tsx
  54. 9
      libs/remix-ui/run-tab/src/lib/actions/deploy.ts
  55. 13
      libs/remix-ui/run-tab/src/lib/actions/events.ts
  56. 17
      libs/remix-ui/run-tab/src/lib/actions/payload.ts
  57. 2
      libs/remix-ui/run-tab/src/lib/components/account.tsx
  58. 18
      libs/remix-ui/run-tab/src/lib/components/contractDropdownUI.tsx
  59. 277
      libs/remix-ui/run-tab/src/lib/components/contractGUI.tsx
  60. 58
      libs/remix-ui/run-tab/src/lib/components/environment.tsx
  61. 2
      libs/remix-ui/run-tab/src/lib/components/recorderCardUI.tsx
  62. 1
      libs/remix-ui/run-tab/src/lib/constants/index.ts
  63. 3
      libs/remix-ui/run-tab/src/lib/css/run-tab.css
  64. 39
      libs/remix-ui/run-tab/src/lib/reducers/runTab.ts
  65. 14
      libs/remix-ui/run-tab/src/lib/types/index.ts
  66. 81
      libs/remix-ui/settings/src/lib/github-settings.tsx
  67. 19
      libs/remix-ui/settings/src/lib/remix-ui-settings.tsx
  68. 4
      libs/remix-ui/settings/src/lib/settingsAction.ts
  69. 12
      libs/remix-ui/settings/src/types/index.ts
  70. 12
      libs/remix-ui/tooltip-popup/.babelrc
  71. 18
      libs/remix-ui/tooltip-popup/.eslintrc.json
  72. 7
      libs/remix-ui/tooltip-popup/README.md
  73. 1
      libs/remix-ui/tooltip-popup/src/index.ts
  74. 0
      libs/remix-ui/tooltip-popup/src/lib/tooltip-popup.module.css
  75. 27
      libs/remix-ui/tooltip-popup/src/lib/tooltip-popup.tsx
  76. 6
      libs/remix-ui/tooltip-popup/src/types/index.ts
  77. 20
      libs/remix-ui/tooltip-popup/tsconfig.json
  78. 13
      libs/remix-ui/tooltip-popup/tsconfig.lib.json
  79. 55
      libs/remix-ui/workspace/src/lib/actions/index.ts
  80. 23
      libs/remix-ui/workspace/src/lib/actions/payload.ts
  81. 69
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  82. 5
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  83. 38
      libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css
  84. 11
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  85. 73
      libs/remix-ui/workspace/src/lib/reducers/workspace.ts
  86. 97
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  87. 2
      libs/remixd/package.json
  88. 3
      nx.json
  89. 16
      package.json
  90. 3
      tsconfig.base.json
  91. 17
      workspace.json
  92. 84
      yarn.lock

@ -323,7 +323,7 @@ jobs:
environment:
- COMMIT_AUTHOR_EMAIL: "yann@ethereum.org"
- COMMIT_AUTHOR: "Circle CI"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/index.html dist/apps/remix-ide/raw-loader*.js dist/apps/remix-ide/assets dist/apps/remix-ide/main*.js dist/apps/remix-ide/polyfills*.js dist/apps/remix-ide/favicon.ico dist/apps/remix-ide/vendors~app*.js dist/apps/remix-ide/app*.js"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/index.html dist/apps/remix-ide/404.html dist/apps/remix-ide/raw-loader*.js dist/apps/remix-ide/assets dist/apps/remix-ide/main*.js dist/apps/remix-ide/polyfills*.js dist/apps/remix-ide/favicon.ico dist/apps/remix-ide/vendors~app*.js dist/apps/remix-ide/app*.js"
working_directory: ~/remix-project
steps:
@ -352,7 +352,7 @@ jobs:
environment:
- COMMIT_AUTHOR_EMAIL: "yann@ethereum.org"
- COMMIT_AUTHOR: "Circle CI"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/index.html dist/apps/remix-ide/raw-loader*.js dist/apps/remix-ide/assets dist/apps/remix-ide/main*.js dist/apps/remix-ide/polyfills*.js dist/apps/remix-ide/favicon.ico dist/apps/remix-ide/vendors~app*.js dist/apps/remix-ide/app*.js"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/index.html dist/apps/remix-ide/404.html dist/apps/remix-ide/raw-loader*.js dist/apps/remix-ide/assets dist/apps/remix-ide/main*.js dist/apps/remix-ide/polyfills*.js dist/apps/remix-ide/favicon.ico dist/apps/remix-ide/vendors~app*.js dist/apps/remix-ide/app*.js"
working_directory: ~/remix-project
steps:
@ -380,7 +380,7 @@ jobs:
environment:
- COMMIT_AUTHOR_EMAIL: "yann@ethereum.org"
- COMMIT_AUTHOR: "Circle CI"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/index.html dist/apps/remix-ide/raw-loader*.js dist/apps/remix-ide/assets dist/apps/remix-ide/main*.js dist/apps/remix-ide/polyfills*.js dist/apps/remix-ide/favicon.ico dist/apps/remix-ide/vendors~app*.js dist/apps/remix-ide/app*.js"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/index.html dist/apps/remix-ide/404.html dist/apps/remix-ide/raw-loader*.js dist/apps/remix-ide/assets dist/apps/remix-ide/main*.js dist/apps/remix-ide/polyfills*.js dist/apps/remix-ide/favicon.ico dist/apps/remix-ide/vendors~app*.js dist/apps/remix-ide/app*.js"
working_directory: ~/remix-project
steps:

@ -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
@ -43,7 +54,7 @@ Note: It contains the latest supported version of Solidity available at the time
"npm": "^6.14.15"
}
```
* Install [Nx CLI](https://nx.dev/react/cli/overview) globally to enable running **nx executable commands**.
* Install [Nx CLI](https://nx.dev/using-nx/nx-cli) globally to enable running **nx executable commands**.
```bash
yarn global add @nrwl/cli
```
@ -116,7 +127,7 @@ curl https://raw.githubusercontent.com/ethereum/remix-project/master/docker-comp
### Troubleshooting
If you have trouble building the project, make sure that you have the correct version of `node`, `npm` and `nvm`. Also ensure [Nx CLI](https://nx.dev/react/cli/overview) is installed globally.
If you have trouble building the project, make sure that you have the correct version of `node`, `npm` and `nvm`. Also ensure [Nx CLI](https://nx.dev/using-nx/nx-cli) is installed globally.
Run:

@ -2,7 +2,7 @@ import { NightwatchBrowser } from 'nightwatch'
import EventEmitter from 'events'
class CreateContract extends EventEmitter {
command (this: NightwatchBrowser, inputParams: string[]): NightwatchBrowser {
command (this: NightwatchBrowser, inputParams: string): NightwatchBrowser {
this.api.perform((done) => {
createContract(this.api, inputParams, () => {
done()
@ -13,19 +13,11 @@ class CreateContract extends EventEmitter {
}
}
function createContract (browser: NightwatchBrowser, inputParams: string[], callback: VoidFunction) {
if (inputParams.length === 1) {
browser.setValue('.udapp_contractActionsContainerSingle > input', inputParams[0], function () {
function createContract (browser: NightwatchBrowser, inputParams: string, callback: VoidFunction) {
if (inputParams) {
browser.setValue('.udapp_contractActionsContainerSingle > input', inputParams, function () {
browser.click('.udapp_contractActionsContainerSingle > button').pause(500).perform(function () { callback() })
})
} else if (inputParams.length > 1) {
browser.perform((done) => {
for (let i = 0; i < inputParams.length; i++) {
browser.setValue(`div.udapp_multiArg:nth-child(${i + 1}) > input`, inputParams[i])
}
done()
})
.click('div.udapp_multiArg > button').pause(500).perform(function () { callback() })
} else {
browser
.click('.udapp_contractActionsContainerSingle > button')

@ -3,13 +3,13 @@ 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)
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,7 +83,7 @@ module.exports = {
browser
.openFile('Untitled.sol')
.clickLaunchIcon('udapp')
.click('*[data-id="settingsSelectEnvOptions"] *[data-id="External Http Provider"]')
.switchEnvironment('External Http Provider')
.waitForElementPresent('[data-id="basic-http-provider-modal-footer-ok-react"]')
.execute(function () {
const modal = document.querySelector('[data-id="basic-http-provider-modal-footer-ok-react"]') as any
@ -91,13 +91,7 @@ module.exports = {
modal.click()
})
.pause(5000)
.execute(function () {
const env: any = document.getElementById('selectExEnvOptions')
return env.value
}, [], function (result) {
browser.assert.ok(result.value === 'External Http Provider', 'Web3 Provider not selected')
})
.waitForElementContainsText('#selectExEnvOptions button', 'External Http Provider')
.clickLaunchIcon('solidity')
.clickLaunchIcon('udapp')
.pause(2000)

@ -78,7 +78,7 @@ module.exports = {
browser
.openFile('Untitled.sol')
.clickLaunchIcon('udapp')
.click('*[data-id="settingsSelectEnvOptions"] *[data-id="External Http Provider"]')
.switchEnvironment('External Http Provider')
.waitForElementPresent('[data-id="basic-http-provider-modal-footer-ok-react"]')
.execute(function () {
const modal = document.querySelector('[data-id="basic-http-provider-modal-footer-ok-react"]') as any

@ -86,7 +86,7 @@ module.exports = {
.clickLaunchIcon('udapp')
.waitForElementPresent('*[title="Deploy - transact (not payable)"]', 35000)
.selectContract('ERC20')
.createContract(["tokenName", "symbol"])
.createContract('"tokenName", "symbol"')
.debugTransaction(0)
.pause(2000)
.waitForElementVisible('#stepdetail')
@ -115,7 +115,7 @@ module.exports = {
.testContracts('withABIEncoderV2.sol', sources[2]['withABIEncoderV2.sol'], ['test'])
.clickLaunchIcon('udapp')
.selectContract('test')
.createContract([])
.createContract('')
.clearConsole()
.clickInstance(0)
.clickFunction('test1 - transact (not payable)', { types: 'bytes userData', values: '0x000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000015b38da6a701c568545dcfcb03fcb875f56beddc4' })
@ -148,7 +148,7 @@ module.exports = {
.testContracts('locals.sol', sources[3]['locals.sol'], ['testLocals'])
.clickLaunchIcon('udapp')
.waitForElementPresent('*[title="Deploy - transact (not payable)"]', 40000)
.createContract([])
.createContract('')
.pause(2000)
.clearConsole()
.clickInstance(0)
@ -173,7 +173,7 @@ module.exports = {
.pause(2000)
.testContracts('withGeneratedSources.sol', sources[4]['withGeneratedSources.sol'], ['A'])
.clickLaunchIcon('udapp')
.createContract([])
.createContract('')
.clearConsole()
.clickInstance(0)
.clickFunction('f - transact (not payable)', { types: 'uint256[] ', values: '[]' })
@ -214,10 +214,10 @@ module.exports = {
.setSolidityCompilerVersion('soljson-v0.8.7+commit.e28d00a7.js')
.addFile('useDebugNodes.sol', sources[5]['useDebugNodes.sol']) // compile contract
.clickLaunchIcon('udapp')
.click('*[data-id="settingsSelectEnvOptions"] *[data-id="External Http Provider"]') // select web3 provider with debug nodes URL
.clearValue('*[data-id="modalDialogCustomPromp"]')
.setValue('*[data-id="modalDialogCustomPromp"]', 'https://remix-rinkeby.ethdevops.io')
.modalFooterOKClick('basic-http-provider')
.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()
.waitForElementPresent('*[title="Deploy - transact (not payable)"]', 65000) // wait for the compilation to succeed
.clickLaunchIcon('debugger')
.clearValue('*[data-id="debuggerTransactionInput"]')
@ -235,7 +235,7 @@ module.exports = {
.testContracts('reverted.sol', sources[6]['reverted.sol'], ['A', 'B', 'C'])
.clickLaunchIcon('udapp')
.selectContract('A')
.createContract([])
.createContract('')
.pause(500)
.clickInstance(0)
.clickFunction('callA - transact (not payable)')

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

@ -22,7 +22,7 @@ module.exports = {
browser.verifyContracts(['test'])
.clickLaunchIcon('udapp')
.selectContract('test')
.createContract([])
.createContract('')
.getAddressAtPosition(0, (address) => {
console.log('testAutoDeployLib ' + address)
addressRef = address
@ -46,7 +46,7 @@ module.exports = {
.verifyContracts(['test'])
.clickLaunchIcon('udapp')
.selectContract('lib') // deploy lib
.createContract([])
.createContract('')
.perform((done) => {
browser.getAddressAtPosition(0, (address) => {
console.log(address)
@ -74,7 +74,7 @@ function checkDeployShouldFail (browser: NightwatchBrowser, callback: VoidFuncti
.openFile('Untitled5.sol')
.clickLaunchIcon('udapp')
.selectContract('test') // deploy lib
.createContract([])
.createContract('')
.pause(2000)
.getText('div[class^="terminal"]', (value) => {
console.log('value: ', value)
@ -98,7 +98,7 @@ function checkDeployShouldSucceed (browser: NightwatchBrowser, address: string,
.openFile('Untitled5.sol')
.clickLaunchIcon('udapp')
.selectContract('test') // deploy lib
.createContract([])
.createContract('')
.getAddressAtPosition(1, (address) => {
addressRef = address
})

@ -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', 'Remix 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()
@ -48,7 +48,7 @@ module.exports = {
},
'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 (')

@ -3,6 +3,7 @@ import { NightwatchBrowser } from 'nightwatch'
import init from '../helpers/init'
module.exports = {
'@disabled': true,
before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done)
},
@ -11,7 +12,7 @@ module.exports = {
return sources
},
'Run Scenario': function (browser: NightwatchBrowser) {
'Run Scenario #group1': function (browser: NightwatchBrowser) {
let addressRef
browser.addFile('scenario.json', { content: records })
.pause(5000)
@ -36,10 +37,10 @@ module.exports = {
.click('*[data-id="deployAndRunClearInstances"]')
},
'Save scenario': function (browser: NightwatchBrowser) {
'Save scenario #group1': function (browser: NightwatchBrowser) {
browser.testContracts('testRecorder.sol', sources[0]['testRecorder.sol'], ['testRecorder'])
.clickLaunchIcon('udapp')
.createContract(['12'])
.createContract('12')
.clickInstance(0)
.clickFunction('set - transact (not payable)', { types: 'uint256 _p', values: '34' })
.click('.savetransaction')
@ -64,7 +65,7 @@ module.exports = {
})
},
'Record more than one contract': function (browser: NightwatchBrowser) {
'Record more than one contract #group1': function (browser: NightwatchBrowser) {
// deploy 2 contracts (2 different ABIs), save the record, reexecute and test one of the function.
browser
.click('*[data-id="deployAndRunClearInstances"]')
@ -72,11 +73,11 @@ module.exports = {
.clickLaunchIcon('udapp')
.selectContract('t1est')
.pause(1000)
.createContract([])
.createContract('')
.clickInstance(0)
.selectContract('t2est')
.pause(1000)
.createContract([])
.createContract('')
.click('.savetransaction')
.waitForElementVisible('[data-id="udappNotify-modal-footer-ok-react"]')
.execute(function () {
@ -84,6 +85,7 @@ module.exports = {
modalOk.click()
})
.pause(1000)
.click('*[data-id="deployAndRunClearInstances"]') // clear udapp
.click('*[data-id="terminalClearConsole"]') // clear terminal
.click('[data-id="runtransaction"]')
@ -98,7 +100,7 @@ module.exports = {
},
'Run with live "mode"': function (browser: NightwatchBrowser) {
'Run with live "mode" #group1': 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 })

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

@ -33,7 +33,7 @@ module.exports = {
.clickLaunchIcon('udapp')
.pause(5000)
.selectContract('ECVerify')
.createContract([])
.createContract('')
.clickInstance(0)
.perform((done) => {
browser.getAddressAtPosition(0, (address) => {

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

@ -24,7 +24,7 @@ module.exports = {
.clickLaunchIcon('udapp')
.selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c') // this account will be used for this test suite
.selectContract('CheckSpecials')
.createContract([]) // deploy
.createContract('') // deploy
.clickInstance(0)
.perform((done) => {
browser.getAddressAtPosition(0, (address) => {
@ -92,7 +92,7 @@ module.exports = {
.testContracts('receiveOnly.sol', sources[1]['receiveOnly.sol'], ['CheckSpecials'])
.clickLaunchIcon('udapp')
.selectContract('CheckSpecials')
.createContract([])
.createContract('')
.clickInstance(0)
.perform((done) => {
browser.getAddressAtPosition(0, (address) => {
@ -122,7 +122,7 @@ module.exports = {
.testContracts('fallbackOnlyPayable.sol', sources[2]['fallbackOnlyPayable.sol'], ['CheckSpecials'])
.clickLaunchIcon('udapp')
.selectContract('CheckSpecials')
.createContract([])
.createContract('')
.clickInstance(0)
.perform((done) => {
browser.getAddressAtPosition(0, (address) => {
@ -153,7 +153,7 @@ module.exports = {
.testContracts('fallbackOnlyNotPayable.sol', sources[3]['fallbackOnlyNotPayable.sol'], ['CheckSpecials'])
.clickLaunchIcon('udapp')
.selectContract('CheckSpecials')
.createContract([])
.createContract('')
.clickInstance(0)
.perform((done) => {
browser.getAddressAtPosition(0, (address) => {
@ -174,7 +174,7 @@ module.exports = {
.clearValue('#value')
.setValue('#value', '0')
.pause(2000)
.createContract([])
.createContract('')
.pause(1000)
.clickInstance(0).pause(1000)
.perform((done) => {
@ -208,7 +208,7 @@ module.exports = {
.waitForElementVisible('#value')
.clearValue('#value')
.setValue('#value', '0').pause(2000)
.createContract([])
.createContract('')
.clickInstance(0)
.pause(1000)
.perform((done) => {

@ -46,11 +46,11 @@ module.exports = {
.waitForElementContainsText('*[data-id="terminalJournal"]', '["0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2","0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db","0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB","0x617F2E2fD72FD9D5503197092aC168c91465E7f2","0x17F6AD8Ef982297579C203069C1DbfFE4348c372","0x5c6B0f7Bf3E7ce046039Bd8FABdfD3f9F5021678","0x03C6FcED478cBbC9a4FAB34eF9f40767739D1Ff7","0x1aE0EA34a72D944a8C7603FfB3eC30a6669E454C","0x0A098Eda01Ce92ff4A4CCb7A4fFFb5A43EBC70DC","0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c","0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C","0x4B0897b0513fdC7C541B6d9D7E929C4e5364D2dB","0x583031D1113aD414F02576BD6afaBfb302140225","0xdD870fA1b7C4700F2BD7f44238821C26f7392148"]')
},
'Call web3.eth.getAccounts() using Web3 Provider #group5': function (browser: NightwatchBrowser) {
'Call web3.eth.getAccounts() using External Http Provider #group5': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="terminalClearConsole"]') // clear the terminal
.clickLaunchIcon('udapp')
.click('*[data-id="settingsSelectEnvOptions"] *[data-id="External Http Provider"]')
.switchEnvironment('External Http Provider')
.modalFooterOKClick('basic-http-provider')
.ExecuteScriptInTerminal('web3.eth.getAccounts()')
.waitForElementContainsText('*[data-id="terminalJournal"]', '["', 60000) // we check if an array is present, don't need to check for the content
@ -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
@ -172,7 +172,7 @@ module.exports = {
.clickLaunchIcon('udapp')
.click('*[data-id="deployAndRunClearInstances"]')
.selectContract('OwnerTest')
.createContract([])
.createContract('')
.pause(1000)
.journalChildIncludes('constructor', { shouldHaveOnlyOneOccurence: true })
.pause(5000)

@ -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)
@ -202,10 +202,10 @@ module.exports = {
.addFile('Storage.sol', sources[6]['Storage.sol'])
.addFile('Owner.sol', sources[6]['Owner.sol'])
.clickLaunchIcon('udapp')
.createContract(['42', '24'])
.createContract('42, 24')
.openFile('Storage.sol')
.clickLaunchIcon('udapp')
.createContract(['102']) // this creation will fail if the component hasn't been properly reset.
.createContract('102') // this creation will fail if the component hasn't been properly reset.
.clickInstance(1)
.clickFunction('store - transact (not payable)', { types: 'uint256 num', values: '24' })
.testFunction('last', // we check if the contract is actually reachable.

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

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

@ -34,7 +34,7 @@ declare module 'nightwatch' {
getModalBody(callback: (value: string, cb: VoidFunction) => void): NightwatchBrowser,
modalFooterCancelClick(id?: string): NightwatchBrowser,
selectContract(contractName: string): NightwatchBrowser,
createContract(inputParams: string[]): NightwatchBrowser,
createContract(inputParams: string): NightwatchBrowser,
getAddressAtPosition(index: number, cb: (pos: string) => void): NightwatchBrowser,
testConstantFunction(address: string, fnFullName: string, expectedInput: NightwatchTestConstantFunctionExpectedInput | null, expectedOutput: string): NightwatchBrowser,
getEditorValue(callback: (content: string) => void): NightwatchBrowser,
@ -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 {

@ -14,7 +14,7 @@ echo "To use an offline copy, download \`remix-$SHA.zip\`." >> README.md
cp -r $FILES_TO_PACKAGE "./"
rm -rf dist
ls
FILES_TO_DEPLOY="assets index.html main*.js polyfills*.js favicon.ico vendors~app*.js app*.js raw-loader*.js"
FILES_TO_DEPLOY="assets index.html 404.html main*.js polyfills*.js favicon.ico vendors~app*.js app*.js raw-loader*.js"
# ZIP the whole directory
zip -r remix-$SHA.zip $FILES_TO_DEPLOY
# -f is needed because "build" is part of .gitignore

@ -14,7 +14,7 @@ echo "To use an offline copy, download \`remix-$SHA.zip\`." >> README.md
cp -r $FILES_TO_PACKAGE "./"
rm -rf dist
ls
FILES_TO_DEPLOY="assets index.html main*.js polyfills*.js favicon.ico vendors~app*.js app*.js raw-loader*.js"
FILES_TO_DEPLOY="assets index.html 404.html main*.js polyfills*.js favicon.ico vendors~app*.js app*.js raw-loader*.js"
# ZIP the whole directory
zip -r remix-$SHA.zip $FILES_TO_DEPLOY
# -f is needed because "build" is part of .gitignore

@ -14,7 +14,7 @@ echo "To use an offline copy, download \`remix-$SHA.zip\`." >> README.md
cp -r $FILES_TO_PACKAGE "./"
rm -rf dist
ls
FILES_TO_DEPLOY="assets index.html main*.js polyfills*.js vendors~app*.js app*.js raw-loader*.js"
FILES_TO_DEPLOY="assets index.html 404.html main*.js polyfills*.js vendors~app*.js app*.js raw-loader*.js"
# ZIP the whole directory
zip -r remix-$SHA.zip $FILES_TO_DEPLOY
# -f is needed because "build" is part of .gitignore

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body></body>
<script>
let route = window.location.pathname
if (route.startsWith('/address/0x') && route.length === 51) {
window.location.href = '/#address=' + route.replace('/address/', '')
} else if (route.endsWith('.sol')) {
window.location.href = '/#url=https://github.com' + route
} else window.location.href = '/'
</script>
</html>

@ -31,6 +31,8 @@ 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')
@ -183,6 +185,8 @@ class AppComponent {
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({
@ -244,6 +248,8 @@ class AppComponent {
ganacheProvider,
foundryProvider,
externalHttpProvider,
injected0ptimismProvider,
injectedArbitrumOneProvider,
this.walkthroughService,
search
])

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

@ -152,6 +152,9 @@ module.exports = class Filepanel extends ViewPlugin {
const workspaceProvider = this.fileProviders.workspace
this.currentWorkspaceMetadata = { name: workspace.name, isLocalhost: workspace.isLocalhost, absolutePath: `${workspaceProvider.workspacesPath}/${workspace.name}` }
if (workspace.name !== " - connect to localhost - ") {
localStorage.setItem('currentWorkspace', workspace.name)
}
this.emit('setWorkspace', workspace)
}

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

@ -30,7 +30,7 @@ export class ExternalHttpProvider extends AbstractProvider {
<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 External HTTP Provider</a>
<br />
<br />
External HTTP Provider Endpoint

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

@ -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 {
@ -169,6 +173,36 @@ export class RunTab extends ViewPlugin {
}
}
})
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) {

@ -11,8 +11,7 @@ import InjectedProvider from './providers/injected.js'
import NodeProvider from './providers/node.js'
import { execution, EventManager, helpers } from '@remix-project/remix-lib'
import { etherScanLink } from './helper'
import { logBuilder, confirmProxyMsg } from "@remix-ui/helper"
import { cancelProxyMsg } from '@remix-ui/helper'
import { logBuilder, cancelUpgradeMsg, cancelProxyMsg } from "@remix-ui/helper"
const { txFormat, txExecution, typeConversion, txListener: Txlistener, TxRunner, TxRunnerWeb3, txHelper } = execution
const { txResultHelper: resultToRemixTx } = helpers
const packageJson = require('../../../../package.json')
@ -150,9 +149,11 @@ export class Blockchain extends Plugin {
cancelLabel: 'Cancel',
okFn: () => {
this.runProxyTx(proxyData, implementationContractObject)
_paq.push(['trackEvent', 'blockchain', 'Deploy With Proxy', 'modal ok confirmation'])
},
cancelFn: () => {
this.call('notification', 'toast', cancelProxyMsg())
_paq.push(['trackEvent', 'blockchain', 'Deploy With Proxy', 'cancel proxy deployment'])
},
hideFn: () => null
}
@ -161,7 +162,9 @@ export class Blockchain extends Plugin {
async runProxyTx (proxyData, implementationContractObject) {
const args = { useCall: false, data: proxyData }
let networkInfo
const confirmationCb = (network, tx, gasEstimation, continueTxExecution, cancelCb) => {
networkInfo = network
// continue using original authorization given by user
continueTxExecution(null)
}
@ -171,14 +174,60 @@ export class Blockchain extends Plugin {
if (error) {
const log = logBuilder(error)
_paq.push(['trackEvent', 'blockchain', 'Deploy With Proxy', 'Proxy deployment failed: ' + error])
return this.call('terminal', 'logHtml', log)
}
if (networkInfo.name === 'VM') this.config.set('vm/proxy', address)
else this.config.set(`${networkInfo.name}/${networkInfo.currentFork}/${networkInfo.id}/proxy`, address)
_paq.push(['trackEvent', 'blockchain', 'Deploy With Proxy', 'Proxy deployment successful'])
return this.call('udapp', 'resolveContractAndAddInstance', implementationContractObject, address)
}
this.runTx(args, confirmationCb, continueCb, promptCb, finalCb)
}
async upgradeProxy(proxyAddress, newImplAddress, data, newImplementationContractObject) {
const upgradeModal = {
id: 'confirmProxyDeployment',
title: 'ERC1967',
message: `Confirm you want to upgrade your contract to new implementation ${newImplAddress}.`,
modalType: 'modal',
okLabel: 'OK',
cancelLabel: 'Cancel',
okFn: () => {
this.runUpgradeTx(proxyAddress, data, newImplementationContractObject)
_paq.push(['trackEvent', 'blockchain', 'Upgrade With Proxy', 'proxy upgrade confirmation click'])
},
cancelFn: () => {
this.call('notification', 'toast', cancelUpgradeMsg())
_paq.push(['trackEvent', 'blockchain', 'Upgrade With Proxy', 'proxy upgrade cancel click'])
},
hideFn: () => null
}
this.call('notification', 'modal', upgradeModal)
}
async runUpgradeTx (proxyAddress, data, newImplementationContractObject) {
const args = { useCall: false, data, to: proxyAddress }
const confirmationCb = (network, tx, gasEstimation, continueTxExecution, cancelCb) => {
// continue using original authorization given by user
continueTxExecution(null)
}
const continueCb = (error, continueTxExecution, cancelCb) => { continueTxExecution() }
const promptCb = (okCb, cancelCb) => { okCb() }
const finalCb = (error, txResult, address, returnValue) => {
if (error) {
const log = logBuilder(error)
_paq.push(['trackEvent', 'blockchain', 'Upgrade With Proxy', 'Upgrade failed'])
return this.call('terminal', 'logHtml', log)
}
_paq.push(['trackEvent', 'blockchain', 'Upgrade With Proxy', 'Upgrade Successful'])
return this.call('udapp', 'resolveContractAndAddInstance', newImplementationContractObject, proxyAddress)
}
this.runTx(args, confirmationCb, continueCb, promptCb, finalCb)
}
async getEncodedFunctionHex (args, funABI) {
return new Promise((resolve, reject) => {
txFormat.encodeFunctionCall(args, funABI, (error, data) => {

@ -152,9 +152,12 @@ export class ExecutionContext {
if (context === 'injected') {
if (injectedProvider === undefined) {
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).')
infoCb('No injected 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)
@ -166,10 +169,20 @@ export class ExecutionContext {
if (this.customNetWorks[context]) {
var network = this.customNetWorks[context]
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()
}
}
}

@ -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', 'foundry-provider', 'basic-http-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)
}

@ -89,3 +89,17 @@ export const UUPSfunAbi = {
outputs: [],
stateMutability: "payable"
}
export const UUPSupgradeAbi = {
"inputs": [
{
"internalType": "address",
"name": "newImplementation",
"type": "address"
}
],
"name": "upgradeTo",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}

@ -1,12 +1,12 @@
import { Plugin } from '@remixproject/engine';
import { ContractABI, ContractAST, DeployOption } from '../types/contract';
import { UUPS, UUPSABI, UUPSBytecode, UUPSfunAbi } from './constants/uups';
import { Plugin } from '@remixproject/engine'
import { ContractABI, ContractAST, ContractSources, DeployOptions } from '../types/contract'
import { UUPS, UUPSABI, UUPSBytecode, UUPSfunAbi, UUPSupgradeAbi } from './constants/uups'
const proxyProfile = {
name: 'openzeppelin-proxy',
displayName: 'openzeppelin-proxy',
description: 'openzeppelin-proxy',
methods: ['isConcerned', 'execute', 'getDeployOptions']
methods: ['isConcerned', 'executeUUPSProxy', 'executeUUPSContractUpgrade', 'getProxyOptions', 'getUpgradeOptions']
};
export class OpenZeppelinProxy extends Plugin {
blockchain: any
@ -28,26 +28,37 @@ export class OpenZeppelinProxy extends Plugin {
return false
}
async getDeployOptions (contracts: ContractABI): Promise<{ [name: string]: DeployOption }> {
async getProxyOptions (data: ContractSources, file: string): Promise<{ [name: string]: DeployOptions }> {
const contracts = data.contracts[file]
const ast = data.sources[file].ast
const inputs = {}
if (this.kind === 'UUPS') {
Object.keys(contracts).map(name => {
if (ast) {
const UUPSSymbol = ast.exportedSymbols['UUPSUpgradeable'] ? ast.exportedSymbols['UUPSUpgradeable'][0] : null
ast.absolutePath === file && ast.nodes.map((node) => {
if (node.name === name && node.linearizedBaseContracts.includes(UUPSSymbol)) {
const abi = contracts[name].abi
const initializeInput = abi.find(node => node.name === 'initialize')
if (initializeInput) {
inputs[name] = {
options: [{ title: 'Deploy with Proxy', active: false }, { title: 'Upgrade with Proxy', active: false }],
initializeOptions: {
inputs: initializeInput,
initializeInputs: this.blockchain.getInputs(initializeInput)
initializeInputs: initializeInput ? this.blockchain.getInputs(initializeInput) : null
}
}
}
})
}
})
}
return inputs
}
async execute(implAddress: string, args: string | string [] = '', initializeABI, implementationContractObject): Promise<void> {
async executeUUPSProxy(implAddress: string, args: string | string [] = '', initializeABI, implementationContractObject): Promise<void> {
// deploy the proxy, or use an existing one
if (!initializeABI) throw new Error('Cannot deploy proxy: Missing initialize ABI')
args = args === '' ? [] : args
@ -56,6 +67,13 @@ export class OpenZeppelinProxy extends Plugin {
if (this.kind === 'UUPS') this.deployUUPSProxy(implAddress, _data, implementationContractObject)
}
async executeUUPSContractUpgrade (proxyAddress: string, newImplAddress: string, newImplementationContractObject): Promise<void> {
if (!newImplAddress) throw new Error('Cannot upgrade: Missing implementation address')
if (!proxyAddress) throw new Error('Cannot upgrade: Missing proxy address')
if (this.kind === 'UUPS') this.upgradeUUPSProxy(proxyAddress, newImplAddress, newImplementationContractObject)
}
async deployUUPSProxy (implAddress: string, _data: string, implementationContractObject): Promise<void> {
const args = [implAddress, _data]
const constructorData = await this.blockchain.getEncodedParams(args, UUPSfunAbi)
@ -74,4 +92,20 @@ export class OpenZeppelinProxy extends Plugin {
implementationContractObject.name = proxyName
this.blockchain.deployProxy(data, implementationContractObject)
}
async upgradeUUPSProxy (proxyAddress: string, newImplAddress: string, newImplementationContractObject): Promise<void> {
const fnData = await this.blockchain.getEncodedFunctionHex([newImplAddress], UUPSupgradeAbi)
const proxyName = 'ERC1967Proxy'
const data = {
contractABI: UUPSABI,
contractName: proxyName,
funAbi: UUPSupgradeAbi,
funArgs: [newImplAddress],
linkReferences: {},
dataHex: fnData.replace('0x', '')
}
// re-use implementation contract's ABI for UI display in udapp and change name to proxy name.
newImplementationContractObject.name = proxyName
this.blockchain.upgradeProxy(proxyAddress, newImplAddress, data, newImplementationContractObject)
}
}

@ -54,16 +54,14 @@ export interface ContractAST {
}[]
}
export interface ContractABI {
[key: string]: {
abi: ({
inputs: never[];
export type ContractABI = {
inputs: [];
stateMutability: string;
type: string;
anonymous?: undefined;
name?: undefined;
name?: string;
outputs?: undefined;
} | {
} | {
anonymous: boolean;
inputs: {
indexed: boolean;
@ -75,7 +73,7 @@ export interface ContractABI {
type: string;
stateMutability?: undefined;
outputs?: undefined;
} | {
} | {
inputs: {
internalType: string;
name: string;
@ -90,52 +88,10 @@ export interface ContractABI {
stateMutability: string;
type: string;
anonymous?: undefined;
})[];
devdoc: {
kind: string;
methods: {
[key: string]: {
[key: string]: string
}
};
version: number;
};
evm: any
metadata: string;
storageLayout: {
storage: {
astId: number;
contract: string;
label: string;
offset: number;
slot: string;
type: string;
}[];
types: {
[key: string]: {
base: string;
encoding: string;
label: string;
numberOfBytes: string;
members?: {
astId: number;
contract: string;
label: string;
offset: number;
slot: string;
type: string;
}[];
};
};
};
userdoc: {
kind: string;
methods: any;
version: number;
};
};
}
export type DeployMode = 'Deploy with Proxy' | 'Upgrade with Proxy'
export type DeployOption = {
initializeInputs: string,
inputs: {
@ -151,4 +107,79 @@ export type DeployOption = {
payable?: boolean,
constant?: any
}
}
export interface DeployOptions {
initializeOptions: DeployOption,
options: { title: DeployMode, active: boolean }[]
}
export interface ContractSources {
contracts: {
[path: string]: {
[contractName: string]: {
abi: ContractABI[],
devdoc: {
kind: string
methods: {
[key: string]: {
[key: string]: string
}
};
version: number
}
evm: any
metadata: string
storageLayout: {
storage: {
astId: number
contract: string
label: string
offset: number
slot: string
type: string
}[]
types: {
[key: string]: {
base: string
encoding: string
label: string
numberOfBytes: string
members?: {
astId: number
contract: string
label: string
offset: number
slot: string
type: string
}[]
}
}
}
userdoc: {
kind: string
methods: any
version: number
}
}
}
},
error: {
component: string,
errorCode: string,
formattedMessage: string,
message: string,
severity: string,
sourceLocation: {
end: number,
file: string,
start: number
},
type: string
}[],
sources: {
[path: string]: {
ast: ContractAST,
id: number
}
}
}

@ -1,3 +1,4 @@
export * from './lib/remix-ui-helper'
export * from './lib/helper-components'
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>
)
},
)

@ -89,3 +89,21 @@ export const cancelProxyMsg = () => (
<b>Proxy deployment cancelled.</b>
</div>
)
export const cancelUpgradeMsg = () => (
<div>
<b>Upgrade with proxy cancelled.</b>
</div>
)
export const deployWithProxyMsg = () => (
<div>
NOTE: Deploy With Proxy will initiate two (2) transactions. The first is for the deployment of your implementation contract and the second will be a deployment of an ERC1967 proxy contract.
</div>
)
export const upgradeWithProxyMsg = () => (
<div>
NOTE: Upgrade With Proxy will initiate two (2) transactions. The first is for the deployment of your implementation contract and the second will intiate a call to the upgradeTo function in your proxy contract.
</div>
)

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

@ -5,9 +5,10 @@ import './panel.css'
export interface RemixPanelProps {
plugins: Record<string, PluginRecord>;
}
}
const RemixUIPanelHeader = (props: RemixPanelProps) => {
const [plugin, setPlugin] = useState<PluginRecord>()
const [toggleExpander, setToggleExpander] = useState<boolean>(false)
useEffect(() => {
if (props.plugins) {
@ -18,9 +19,48 @@ const RemixUIPanelHeader = (props: RemixPanelProps) => {
}
}, [props])
const toggleClass = () => {
setToggleExpander(!toggleExpander)
}
return (
<header className='swapitHeader'><h6 data-id='sidePanelSwapitTitle'>{plugin?.profile.displayName || plugin?.profile.name}</h6>
{plugin?.profile.documentation ? (<a href={plugin.profile.documentation} className="titleInfo mb-2" title="link to documentation" target="_blank" rel="noreferrer"><i aria-hidden="true" className="fas fa-book"></i></a>) : ''}
<header className='d-flex flex-column'>
<div className="swapitHeader px-3 pt-3 pb-0 d-flex flex-row">
<h6 data-id='sidePanelSwapitTitle'>{plugin?.profile.displayName || plugin?.profile.name}</h6>
<div className="d-flex flex-row">
<div className="d-flex flex-row">
{plugin?.profile?.maintainedBy?.toLowerCase() === "remix" && (<i aria-hidden="true" className="text-success fas fa-check" title="Maintained by Remix"></i>)}
{plugin?.profile.documentation && (<a href={plugin.profile.documentation} className="titleInfo mb-2" title="link to documentation" target="_blank" rel="noreferrer"><i aria-hidden="true" className="fas fa-book"></i></a>)}
</div>
<div className="swapitHeaderInfoSection d-flex justify-content-between" data-id='swapitHeaderInfoSectionId' onClick={toggleClass} title="Plugin info">
<i className={`px-2 ml-2 pt-1 ${!toggleExpander ? 'fas fa-angle-right' : 'fas fa-angle-down bg-light'}`} aria-hidden="true"></i>
</div>
</div>
</div>
<div className={`bg-light p-3 pt-1 border-bottom flex-column ${toggleExpander ? "d-flex" : "d-none"}`}>
{plugin?.profile?.author && <span className="d-flex flex-row align-items-center">
<label className="mb-0 pr-2">Author:</label>
<span> { plugin?.profile.author } </span>
</span>}
{plugin?.profile?.maintainedBy && <span className="d-flex flex-row align-items-center">
<label className="mb-0 pr-2">Maintained by:</label>
<span> { plugin?.profile.maintainedBy } </span>
</span>}
{plugin?.profile?.documentation && <span className="d-flex flex-row align-items-center">
<label className="mb-0 pr-2">Documentation:</label>
<span>
<a href={plugin?.profile?.documentation} className="titleInfo mb-2" title="link to documentation" target="_blank" rel="noreferrer"><i aria-hidden="true" className="fas fa-book"></i></a>
</span>
</span>}
{plugin?.profile?.description && <span className="d-flex flex-row align-items-baseline">
<label className="mb-0 pr-2">Description:</label>
<span> { plugin?.profile.description } </span>
</span>}
{plugin?.profile?.repo && <span className="d-flex flex-row align-items-center">
<a href={plugin?.profile?.repo} target="_blank" rel="noreferrer">
Make an issue</a>
</span>}
</div>
</header>)
}

@ -22,7 +22,6 @@
.swapitHeader {
display: flex;
align-items: center;
padding: 16px 24px 15px;
justify-content: space-between;
text-transform: uppercase;
}

@ -19,6 +19,9 @@ function ActivePluginCard ({
<h6 className="remixui_displayName plugin-name">
<div>
{ profile.displayName || profile.name }
{ profile?.maintainedBy?.toLowerCase() == "remix" &&
<i aria-hidden="true" className="px-1 text-success fas fa-check" title="Maintained by Remix"></i>
}
{ profile.documentation &&
<a href={profile.documentation} className="px-1" title="link to documentation" target="_blank" rel="noreferrer">
<i aria-hidden="true" className="fas fa-book"/>

@ -22,6 +22,9 @@ function InactivePluginCard ({
<h6 className="remixui_displayName plugin-name">
<div>
{ profile.displayName || profile.name }
{ profile?.maintainedBy?.toLowerCase() == "remix" &&
<i aria-hidden="true" className="px-1 text-success fas fa-check" title="Verified by Remix"></i>
}
{ profile.documentation &&
<a href={profile.documentation} className="px-1" title="link to documentation" target="_blank" rel="noreferrer">
<i aria-hidden="true" className="fas fa-book"/>

@ -191,7 +191,7 @@ function LocalPluginForm ({ closeModal, visible, pluginManager }: LocalPluginFor
type="radio"
name="location"
value="sidePanel"
id="none"
id="localPluginRadioButtonsidePanelSidePanel"
data-id='localPluginRadioButtonsidePanel'
checked={location === 'sidePanel'}
onChange={(e) => setLocation(e.target.value as 'sidePanel' | 'mainPanel' | 'none')} />
@ -203,7 +203,7 @@ function LocalPluginForm ({ closeModal, visible, pluginManager }: LocalPluginFor
type="radio"
name="location"
value="mainPanel"
id="none"
id="localPluginRadioButtonsidePanelMainPanel"
data-id='localPluginRadioButtonmainPanel'
checked={location === 'mainPanel'}
onChange={(e) => setLocation(e.target.value as 'sidePanel' | 'mainPanel' | 'none')} />
@ -215,7 +215,7 @@ function LocalPluginForm ({ closeModal, visible, pluginManager }: LocalPluginFor
type="radio"
name="location"
value="none"
id="none"
id="localPluginRadioButtonsidePanelNone"
data-id='localPluginRadioButtonnone'
checked={location === 'none'}
onChange={(e) => setLocation(e.target.value as 'sidePanel' | 'mainPanel' | 'none')} />

@ -135,6 +135,7 @@ export const createInstance = async (
args,
deployMode: DeployMode[]) => {
const isProxyDeployment = (deployMode || []).find(mode => mode === 'Deploy with Proxy')
const isContractUpgrade = (deployMode || []).find(mode => mode === 'Upgrade with Proxy')
const statusCb = (msg: string) => {
const log = logBuilder(msg)
@ -160,7 +161,9 @@ export const createInstance = async (
if (isProxyDeployment) {
const initABI = contractObject.abi.find(abi => abi.name === 'initialize')
plugin.call('openzeppelin-proxy', 'execute', addressToString(address), args, initABI, contractObject)
plugin.call('openzeppelin-proxy', 'executeUUPSProxy', addressToString(address), args, initABI, contractObject)
} else if (isContractUpgrade) {
plugin.call('openzeppelin-proxy', 'executeUUPSContractUpgrade', args, addressToString(address), contractObject)
}
}
@ -176,7 +179,7 @@ export const createInstance = async (
if (selectedContract.isOverSizeLimit()) {
return dispatch(displayNotification('Contract code size over limit', isOverSizePrompt(), 'Force Send', 'Cancel', () => {
deployContract(plugin, selectedContract, !isProxyDeployment ? args : '', contractMetadata, compilerContracts, {
deployContract(plugin, selectedContract, !isProxyDeployment && !isContractUpgrade ? args : '', contractMetadata, compilerContracts, {
continueCb: (error, continueTxExecution, cancelCb) => {
continueHandler(dispatch, gasEstimationPrompt, error, continueTxExecution, cancelCb)
},
@ -192,7 +195,7 @@ export const createInstance = async (
return terminalLogger(plugin, log)
}))
}
deployContract(plugin, selectedContract, !isProxyDeployment ? args : '', contractMetadata, compilerContracts, {
deployContract(plugin, selectedContract, !isProxyDeployment && !isContractUpgrade ? args : '', contractMetadata, compilerContracts, {
continueCb: (error, continueTxExecution, cancelCb) => {
continueHandler(dispatch, gasEstimationPrompt, error, continueTxExecution, cancelCb)
},

@ -2,7 +2,7 @@ import { envChangeNotification } from "@remix-ui/helper"
import { RunTab } from "../types/run-tab"
import { setExecutionContext, setFinalContext, updateAccountBalances } from "./account"
import { addExternalProvider, addInstance, removeExternalProvider, setNetworkNameFromProvider } from "./actions"
import { clearAllInstances, clearRecorderCount, fetchContractListSuccess, resetUdapp, setCurrentContract, setCurrentFile, setDeployOptions, setLoadType, setRecorderCount, setSendValue } from "./payload"
import { addDeployOption, clearAllInstances, clearRecorderCount, fetchContractListSuccess, resetUdapp, setCurrentContract, setCurrentFile, setLoadType, setProxyEnvAddress, setRecorderCount, setSendValue } from "./payload"
import { CompilerAbstract } from '@remix-project/remix-solidity'
import * as ethJSUtil from 'ethereumjs-util'
import Web3 from 'web3'
@ -24,7 +24,7 @@ export const setupEvents = (plugin: RunTab, dispatch: React.Dispatch<any>) => {
plugin.blockchain.event.register('networkStatus', ({ error, network }) => {
if (error) {
const netUI = 'can\'t detect network '
const netUI = 'can\'t detect network'
setNetworkNameFromProvider(dispatch, netUI)
return
@ -33,6 +33,8 @@ export const setupEvents = (plugin: RunTab, dispatch: React.Dispatch<any>) => {
const netUI = (networkProvider() !== 'vm') ? `${network.name} (${network.id || '-'}) network` : 'VM'
setNetworkNameFromProvider(dispatch, netUI)
if (network.name === 'VM') dispatch(setProxyEnvAddress(plugin.config.get('vm/proxy')))
else dispatch(setProxyEnvAddress(plugin.config.get(`${network.name}/${network.currentFork}/${network.id}/proxy`)))
})
plugin.blockchain.event.register('addProvider', provider => addExternalProvider(dispatch, provider))
@ -106,11 +108,12 @@ const broadcastCompilationResult = async (plugin: RunTab, dispatch: React.Dispat
const isUpgradeable = await plugin.call('openzeppelin-proxy', 'isConcerned', data.sources[file] ? data.sources[file].ast : {})
if (isUpgradeable) {
const options = await plugin.call('openzeppelin-proxy', 'getDeployOptions', data.contracts[file])
const options = await plugin.call('openzeppelin-proxy', 'getProxyOptions', data, file)
dispatch(setDeployOptions({ options: [{ title: 'Deploy with Proxy', active: false }], initializeOptions: options }))
dispatch(addDeployOption({ [file]: options }))
} else {
dispatch(addDeployOption({ [file]: {} }))
}
else dispatch(setDeployOptions({} as any))
dispatch(fetchContractListSuccess({ [file]: contracts }))
dispatch(setCurrentFile(file))
// TODO: set current contract

@ -1,6 +1,6 @@
import { ContractList } from '../reducers/runTab'
import { ContractData } from '@remix-project/core-plugin'
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, 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 { 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, 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_PROXY_ENV_ADDRESS, SET_RECORDER_COUNT, SET_SELECTED_ACCOUNT, SET_SEND_UNIT, SET_SEND_VALUE, SET_TX_FEE_CONTENT } from '../constants'
import { DeployMode, DeployOptions } from '../types'
export const fetchAccountsListRequest = () => {
@ -280,21 +280,21 @@ export const resetUdapp = () => {
}
}
export const addDeployOption = (deployOption: DeployOptions) => {
export const addDeployOption = (deployOption: { [file: string]: { [name: string]: DeployOptions } }) => {
return {
payload: deployOption,
type: ADD_DEPLOY_OPTION
}
}
export const removeDeployOption = (title: DeployMode) => {
export const removeDeployOption = (file: string) => {
return {
payload: title,
payload: file,
type: REMOVE_DEPLOY_OPTION
}
}
export const setDeployOptions = (deployOptions: DeployOptions) => {
export const setDeployOptions = (deployOptions: { [file: string]: { [name: string]: DeployOptions } }) => {
return {
payload: deployOptions,
type: SET_DEPLOY_OPTIONS
@ -307,3 +307,10 @@ export const setCurrentContract = (contractName: string) => {
type: SET_CURRENT_CONTRACT
}
}
export const setProxyEnvAddress = (key: string) => {
return {
payload: key,
type: SET_PROXY_ENV_ADDRESS
}
}

@ -22,7 +22,7 @@ export function AccountUI (props: AccountProps) {
case 'injected':
setPlusOpt({
classList: 'udapp_disableMouseEvents',
title: "Unfortunately it's not possible to create an account using injected web3. Please create the account directly from your provider (i.e metamask or other of the same type)."
title: "Unfortunately it's not possible to create an account using injected provider. Please create the account directly from your provider (i.e metamask or other of the same type)."
})
break

@ -4,6 +4,7 @@ import { ContractDropdownProps, DeployMode } from '../types'
import { ContractData, FuncABI } from '@remix-project/core-plugin'
import * as ethJSUtil from 'ethereumjs-util'
import { ContractGUI } from './contractGUI'
import { deployWithProxyMsg, upgradeWithProxyMsg } from '@remix-ui/helper'
export function ContractDropdownUI (props: ContractDropdownProps) {
const [abiLabel, setAbiLabel] = useState<{
@ -27,7 +28,7 @@ export function ContractDropdownUI (props: ContractDropdownProps) {
const [constructorInterface, setConstructorInterface] = useState<FuncABI>(null)
const [constructorInputs, setConstructorInputs] = useState(null)
const contractsRef = useRef<HTMLSelectElement>(null)
const { contractList, loadType, currentFile, currentContract, compilationCount, deployOptions } = props.contracts
const { contractList, loadType, currentFile, currentContract, compilationCount, deployOptions, proxyKey } = props.contracts
useEffect(() => {
enableAtAddress(false)
@ -155,8 +156,18 @@ export function ContractDropdownUI (props: ContractDropdownProps) {
if (selectedContract.bytecodeObject.length === 0) {
return props.modal('Alert', 'This contract may be abstract, it may not implement an abstract parent\'s methods completely or it may not invoke an inherited contract\'s constructor correctly.', 'OK', () => {})
}
if ((selectedContract.name !== currentContract) && (selectedContract.name === 'ERC1967Proxy')) selectedContract.name = currentContract
const isProxyDeployment = (deployMode || []).find(mode => mode === 'Deploy with Proxy')
const isContractUpgrade = (deployMode || []).find(mode => mode === 'Upgrade with Proxy')
if (isProxyDeployment || isContractUpgrade) {
props.modal('ERC1967', isProxyDeployment ? deployWithProxyMsg() : upgradeWithProxyMsg(), 'Proceed', () => {
props.createInstance(loadedContractData, props.gasEstimationPrompt, props.passphrasePrompt, props.publishToStorage, props.mainnetPrompt, isOverSizePrompt, args, deployMode)
}, 'Cancel', () => {})
} else {
props.createInstance(loadedContractData, props.gasEstimationPrompt, props.passphrasePrompt, props.publishToStorage, props.mainnetPrompt, isOverSizePrompt, args, deployMode)
}
}
const atAddressChanged = (event) => {
const value = event.target.value
@ -232,14 +243,15 @@ export function ContractDropdownUI (props: ContractDropdownProps) {
<ContractGUI
title='Deploy'
isDeploy={true}
deployOption={deployOptions.options}
initializerOptions={deployOptions.initializeOptions ? deployOptions.initializeOptions[currentContract] : null}
deployOption={deployOptions[currentFile] && deployOptions[currentFile][currentContract] ? deployOptions[currentFile][currentContract].options : null}
initializerOptions={deployOptions[currentFile] && deployOptions[currentFile][currentContract] ? deployOptions[currentFile][currentContract].initializeOptions : null}
funcABI={constructorInterface}
clickCallBack={clickCallback}
inputs={constructorInputs}
widthClass='w-50'
evmBC={loadedContractData.bytecodeObject}
lookupOnly={false}
savedProxyAddress={proxyKey}
/>
<div className="d-flex py-1 align-items-center custom-control custom-checkbox">
<input

@ -3,8 +3,6 @@ import React, { useEffect, useRef, useState } from 'react'
import * as remixLib from '@remix-project/remix-lib'
import { ContractGUIProps } from '../types'
import { CopyToClipboard } from '@remix-ui/clipboard'
import { MultiDeployInput } from './multiDeployInput'
import { DeployInput } from './deployInput'
const txFormat = remixLib.execution.txFormat
export function ContractGUI (props: ContractGUIProps) {
@ -15,20 +13,15 @@ export function ContractGUI (props: ContractGUIProps) {
title: string,
content: string,
classList: string,
dataId: string,
widthClass: string
}>({ title: '', content: '', classList: '', dataId: '', widthClass: '' })
const [selectedDeployIndex, setSelectedDeployIndex] = useState<number>(null)
const [showOptions, setShowOptions] = useState<boolean>(false)
const [hasArgs, setHasArgs] = useState<boolean>(false)
const [isMultiField, setIsMultiField] = useState<boolean>(false)
const [deployInputs, setDeployInputs] = useState<{
internalType?: string,
name: string,
type: string
}[]>([])
const [deployPlaceholder, setDeployPlaceholder] = useState<string>('')
dataId: string
}>({ title: '', content: '', classList: '', dataId: '' })
const [toggleDeployProxy, setToggleDeployProxy] = useState<boolean>(false)
const [toggleUpgradeImp, setToggleUpgradeImp] = useState<boolean>(false)
const [deployState, setDeployState] = useState<{ deploy: boolean, upgrade: boolean }>({ deploy: false, upgrade: false })
const [useLastProxy, setUseLastProxy] = useState<boolean>(false)
const [proxyAddress, setProxyAddress] = useState<string>('')
const multiFields = useRef<Array<HTMLInputElement | null>>([])
const initializeFields = useRef<Array<HTMLInputElement | null>>([])
const basicInputRef = useRef<HTMLInputElement>()
useEffect(() => {
@ -41,7 +34,7 @@ export function ContractGUI (props: ContractGUIProps) {
}
setBasicInput('')
// we have the reset the fields before reseting the previous references.
if (basicInputRef.current) basicInputRef.current.value = ''
basicInputRef.current.value = ''
multiFields.current.filter((el) => el !== null && el !== undefined).forEach((el) => el.value = '')
multiFields.current = []
}, [props.title, props.funcABI])
@ -53,8 +46,7 @@ export function ContractGUI (props: ContractGUIProps) {
title: title + ' - call',
content: 'call',
classList: 'btn-info',
dataId: title + ' - call',
widthClass: props.widthClass
dataId: title + ' - call'
})
} else if (props.funcABI.stateMutability === 'payable' || props.funcABI.payable) {
// // transact. stateMutability = payable
@ -62,8 +54,7 @@ export function ContractGUI (props: ContractGUIProps) {
title: title + ' - transact (payable)',
content: 'transact',
classList: 'btn-danger',
dataId: title + ' - transact (payable)',
widthClass: props.widthClass
dataId: title + ' - transact (payable)'
})
} else {
// // transact. stateMutability = nonpayable
@ -71,59 +62,13 @@ export function ContractGUI (props: ContractGUIProps) {
title: title + ' - transact (not payable)',
content: 'transact',
classList: 'btn-warning',
dataId: title + ' - transact (not payable)',
widthClass: props.widthClass
dataId: title + ' - transact (not payable)'
})
}
}, [props.lookupOnly, props.funcABI, title])
useEffect(() => {
if (props.deployOption && props.deployOption[selectedDeployIndex]) {
if (props.deployOption[selectedDeployIndex].title === 'Deploy with Proxy') {
if (props.initializerOptions) {
setDeployInputs(props.initializerOptions.inputs.inputs)
setDeployPlaceholder(props.initializerOptions.initializeInputs)
setHasArgs(true)
if (props.initializerOptions.inputs.inputs.length > 1) setIsMultiField(true)
else setIsMultiField(false)
} else {
setDeployInputs([])
setDeployPlaceholder('')
setHasArgs(false)
setIsMultiField(false)
}
} else {
if (props.funcABI) {
setDeployInputs(props.funcABI.inputs)
setDeployPlaceholder(props.inputs)
setHasArgs(true)
if (props.funcABI.inputs.length > 1) setIsMultiField(true)
else setIsMultiField(false)
} else {
setDeployInputs([])
setDeployPlaceholder('')
setHasArgs(false)
setIsMultiField(false)
}
}
} else {
if (props.funcABI) {
setDeployInputs(props.funcABI.inputs)
setDeployPlaceholder(props.inputs)
setHasArgs(true)
if (props.funcABI.inputs.length > 1) setIsMultiField(true)
else setIsMultiField(false)
} else {
setDeployInputs([])
setDeployPlaceholder('')
setHasArgs(false)
setIsMultiField(false)
}
}
}, [selectedDeployIndex, props.funcABI, props.initializerOptions])
const getContentOnCTC = (fields: HTMLInputElement[]) => {
const multiString = getMultiValsString(fields)
const getContentOnCTC = () => {
const multiString = getMultiValsString(multiFields.current)
// copy-to-clipboard icon is only visible for method requiring input params
if (!multiString) {
return 'cannot encode empty arguments'
@ -191,8 +136,7 @@ export function ContractGUI (props: ContractGUIProps) {
if (inputString) {
inputString = inputString.replace(/(^|,\s+|,)(\d+)(\s+,|,|$)/g, '$1"$2"$3') // replace non quoted number by quoted number
inputString = inputString.replace(/(^|,\s+|,)(0[xX][0-9a-fA-F]+)(\s+,|,|$)/g, '$1"$2"$3') // replace non quoted hex string by quoted hex string
inputString = JSON.stringify([inputString])
const inputJSON = JSON.parse(inputString)
const inputJSON = JSON.parse('[' + inputString + ']')
const multiInputs = multiFields.current
for (let k = 0; k < multiInputs.length; k++) {
@ -204,9 +148,15 @@ export function ContractGUI (props: ContractGUIProps) {
}
const handleActionClick = () => {
const deployMode = selectedDeployIndex !== null ? [props.deployOption[selectedDeployIndex].title] : []
if (deployState.deploy) {
const proxyInitializeString = getMultiValsString(initializeFields.current)
props.clickCallBack(props.funcABI.inputs, basicInput, deployMode)
props.clickCallBack(props.initializerOptions.inputs.inputs, proxyInitializeString, ['Deploy with Proxy'])
} else if (deployState.upgrade) {
props.clickCallBack(props.funcABI.inputs, proxyAddress, ['Upgrade with Proxy'])
} else {
props.clickCallBack(props.funcABI.inputs, basicInput)
}
}
const handleBasicInput = (e) => {
@ -215,51 +165,59 @@ export function ContractGUI (props: ContractGUIProps) {
setBasicInput(value)
}
const handleMultiValsSubmit = (fields: HTMLInputElement[]) => {
const valsString = getMultiValsString(fields)
const deployMode = selectedDeployIndex !== null ? [props.deployOption[selectedDeployIndex].title] : []
const handleExpandMultiClick = () => {
const valsString = getMultiValsString(multiFields.current)
if (valsString) {
props.clickCallBack(props.funcABI.inputs, valsString, deployMode)
props.clickCallBack(props.funcABI.inputs, valsString)
} else {
props.clickCallBack(props.funcABI.inputs, '', deployMode)
props.clickCallBack(props.funcABI.inputs, '')
}
}
const setSelectedDeploy = (index: number) => {
setSelectedDeployIndex(index !== selectedDeployIndex ? index : null)
if (basicInputRef.current) basicInputRef.current.value = ''
setBasicInput('')
const handleToggleDeployProxy = () => {
setToggleDeployProxy(!toggleDeployProxy)
}
const handleDeployProxySelect = (e) => {
const value = e.target.checked
if (value) setToggleUpgradeImp(false)
setToggleDeployProxy(value)
setDeployState({ upgrade: false, deploy: value })
}
const handleToggleUpgradeImp = () => {
setToggleUpgradeImp(!toggleUpgradeImp)
}
const handleUpgradeImpSelect = (e) => {
const value = e.target.checked
setToggleUpgradeImp(value)
if (value) {
setToggleDeployProxy(false)
if (useLastProxy) setProxyAddress(props.savedProxyAddress)
}
setDeployState({ deploy: false, upgrade: value })
}
const handleUseLastProxySelect = (e) => {
const value = e.target.checked
const address = props.savedProxyAddress
setUseLastProxy(value)
setProxyAddress(address || '')
}
const toggleOptions = () => {
setShowOptions(!showOptions)
const handleSetProxyAddress = (e) => {
const value = e.target.value
setProxyAddress(value)
}
return (
<div className={`udapp_contractProperty ${hasArgs ? 'udapp_hasArgs' : ''}`}>
{
props.isDeploy ? !isMultiField ?
<DeployInput
buttonOptions={buttonOptions}
funcABI={props.initializerOptions ? props.initializerOptions.inputs : props.funcABI}
inputs={deployPlaceholder}
handleBasicInput={handleBasicInput}
basicInputRef={basicInputRef}
selectedIndex={selectedDeployIndex}
setSelectedIndex={setSelectedDeploy}
handleActionClick={handleActionClick}
deployOptions={props.deployOption}
/> : <MultiDeployInput
buttonOptions={buttonOptions}
selectedIndex={selectedDeployIndex}
setSelectedIndex={setSelectedDeploy}
handleMultiValsSubmit={handleMultiValsSubmit}
inputs={deployInputs}
getMultiValsString={getMultiValsString}
deployOptions={props.deployOption}
/> :
<>
<div className={`udapp_contractProperty ${(props.funcABI.inputs && props.funcABI.inputs.length > 0) || (props.funcABI.type === 'fallback') || (props.funcABI.type === 'receive') ? 'udapp_hasArgs' : ''}`}>
<div className="udapp_contractActionsContainerSingle pt-2" style={{ display: toggleContainer ? 'none' : 'flex' }}>
<button onClick={handleActionClick} title={buttonOptions.title} className={`udapp_instanceButton ${props.widthClass} btn btn-sm ${buttonOptions.classList}`} data-id={buttonOptions.dataId}>{title}</button>
<input
@ -292,12 +250,111 @@ export function ContractGUI (props: ContractGUIProps) {
})}
</div>
<div className="udapp_group udapp_multiArg">
<CopyToClipboard tip='Encode values of input fields & copy to clipboard' icon='fa-clipboard' direction={'bottom'} getContent={() => getContentOnCTC(multiFields.current)} />
<button onClick={() => handleMultiValsSubmit(multiFields.current)} title={buttonOptions.title} data-id={buttonOptions.dataId} className={`udapp_instanceButton ${buttonOptions.classList}`}>{ buttonOptions.content }</button>
<CopyToClipboard tip='Encode values of input fields & copy to clipboard' icon='fa-clipboard' direction={'bottom'} getContent={getContentOnCTC} />
<button onClick={handleExpandMultiClick} title={buttonOptions.title} data-id={buttonOptions.dataId} className={`udapp_instanceButton ${buttonOptions.classList}`}>{ buttonOptions.content }</button>
</div>
</div>
</div>
{ props.deployOption && (props.deployOption || []).length > 0 ?
<>
<div className='d-flex justify-content-between'>
<div className="d-flex py-1 align-items-center custom-control custom-checkbox">
<input
id="deployWithProxy"
data-id="contractGUIDeployWithProxy"
className="form-check-input custom-control-input"
type="checkbox"
onChange={handleDeployProxySelect}
checked={deployState.deploy}
/>
<label
htmlFor="deployWithProxy"
data-id="contractGUIDeployWithProxyLabel"
className="m-0 form-check-label custom-control-label udapp_checkboxAlign"
title="An ERC1967 proxy contract will be deployed along with the selected implementation contract."
>
Deploy With Proxy
</label>
</div>
<div>
{
props.initializerOptions && props.initializerOptions.initializeInputs ?
<span onClick={handleToggleDeployProxy}>
<i className={!toggleDeployProxy ? 'fas fa-angle-right pt-2' : 'fas fa-angle-down'} aria-hidden="true"></i>
</span> : null
}
</div>
</div>
{
props.initializerOptions && props.initializerOptions.initializeInputs ?
<div className={`pl-4 flex-column ${toggleDeployProxy ? "d-flex" : "d-none"}`}>
<div className={`flex-column 'd-flex'}`}>{
props.initializerOptions.inputs.inputs.map((inp, index) => {
return (
<div className="mb-2" key={index}>
<label className='mt-2 text-left d-block' htmlFor={inp.name}> {inp.name}: </label>
<input ref={el => { initializeFields.current[index] = el }} style={{ height: 32 }} className="form-control udapp_input" placeholder={inp.type} title={inp.name} />
</div>
)})
}
</div>
</div> : null
}
<div className='d-flex justify-content-between'>
<div className="d-flex py-1 align-items-center custom-control custom-checkbox">
<input
id="upgradeImplementation"
data-id="contractGUIUpgradeImplementation"
className="form-check-input custom-control-input"
type="checkbox"
onChange={handleUpgradeImpSelect}
checked={deployState.upgrade}
/>
<label
htmlFor="upgradeImplementation"
data-id="contractGUIUpgradeImplementationLabel"
className="m-0 form-check-label custom-control-label udapp_checkboxAlign"
title="The implemetation address will be updated to a new address in the proxy contract."
>
Upgrade With Proxy
</label>
</div>
<span onClick={handleToggleUpgradeImp}>
<i className={!toggleUpgradeImp ? 'fas fa-angle-right pt-2' : 'fas fa-angle-down'} aria-hidden="true"></i>
</span>
</div>
<div className={`pl-4 flex-column ${toggleUpgradeImp ? "d-flex" : "d-none"}`}>
<div className={`flex-column 'd-flex'}`}>
<div className="d-flex py-1 align-items-center custom-control custom-checkbox">
<input
id="proxyAddress"
data-id="contractGUIProxyAddress"
className="form-check-input custom-control-input"
type="checkbox"
onChange={handleUseLastProxySelect}
checked={useLastProxy}
/>
<label
htmlFor="proxyAddress"
data-id="contractGUIProxyAddressLabel"
className="m-0 form-check-label custom-control-label udapp_checkboxAlign"
title="Select this option to use the last deployed ERC1967 contract on the current network."
style={{ fontSize: 12 }}
>
Use last deployed ERC1967 contract
</label>
</div>
{
!useLastProxy ?
<div className="mb-2">
<label className='mt-2 text-left d-block'>Proxy Address: </label>
<input style={{ height: 32 }} className="form-control udapp_input" placeholder='proxy address' title='Enter previously deployed proxy address on the selected network' onChange={handleSetProxyAddress} />
</div> :
<span className='text-capitalize' style={{ fontSize: '.8em' }}>{ proxyAddress || 'No proxy address available' }</span>
}
</div>
</div>
</>
</> : null
}
</div>
)

@ -1,11 +1,16 @@
// 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)
const fork = provider.fork // can be undefined if connected to an external source (External Http Provider / injected)
let context = provider.value
context = context.startsWith('vm') ? 'vm' : context
@ -13,22 +18,55 @@ export function EnvironmentUI (props: EnvironmentProps) {
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) }}>
<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((provider, index) =>
<option id={provider.id} key={index} data-id={provider.dataId}
title={provider.title}
value={provider.value}> { provider.content }
</option>
)
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>
))
}
</select>
</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>

@ -27,7 +27,7 @@ export function RecorderUI (props: RecorderProps) {
<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>Save transactions (deployed contracts and function executions) and replay them in another environment. <br/> e.g Transactions created in Remix VM can be replayed in the Injected Provider.
</span>
</Tooltip>
}>

@ -45,3 +45,4 @@ export const ADD_DEPLOY_OPTION = 'ADD_DEPLOY_OPTION'
export const REMOVE_DEPLOY_OPTION = 'REMOVE_DEPLOY_OPTION'
export const SET_DEPLOY_OPTIONS = 'SET_DEPLOY_OPTIONS'
export const SET_CURRENT_CONTRACT = 'SET_CURRENT_CONTRACT'
export const SET_PROXY_ENV_ADDRESS = 'SET_PROXY_ENV_ADDRESS'

@ -529,4 +529,7 @@
text-decoration: none;
background-color: #007aa6;
}
.udapp_selectExEnvOptions {
width: 100%;
}

@ -1,8 +1,7 @@
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'
import { DeployOptions } from '../types'
import { 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_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, SET_PROXY_ENV_ADDRESS, ADD_DEPLOY_OPTION, REMOVE_DEPLOY_OPTION } from '../constants'
declare const window: any
interface Action {
@ -67,7 +66,8 @@ export interface RunTabState {
compiler: CompilerAbstract
}[]
},
deployOptions: DeployOptions
deployOptions: { [file: string]: { [name: string]: DeployOptions } },
proxyKey: string,
loadType: 'abi' | 'sol' | 'other'
currentFile: string,
currentContract: string,
@ -155,6 +155,7 @@ export const runTabInitialState: RunTabState = {
contracts: {
contractList: {},
deployOptions: {} as any,
proxyKey: '',
loadType: 'other',
currentFile: '',
currentContract: '',
@ -680,39 +681,33 @@ export const runTabReducer = (state: RunTabState = runTabInitialState, action: A
}
case ADD_DEPLOY_OPTION: {
const payload: { title: DeployMode, active: boolean } = action.payload
const payload: { [file: string]: { [name: string]: DeployOptions } } = action.payload
return {
...state,
contracts: {
...state.contracts,
deployOptions: {
...state.contracts.deployOptions,
options: [...state.contracts.deployOptions.options, payload]
}
deployOptions: {...state.contracts.deployOptions, ...payload }
}
}
}
case REMOVE_DEPLOY_OPTION: {
const payload: string = action.payload
const options = state.contracts.deployOptions.options.filter(val => val.title !== payload)
const options = state.contracts.deployOptions
delete options[payload]
return {
...state,
contracts: {
...state.contracts,
deployOptions: {
...state.contracts.deployOptions,
options
}
deployOptions: options
}
}
}
case SET_DEPLOY_OPTIONS: {
const payload: DeployOptions = action.payload
const payload: { [file: string]: { [name: string]: DeployOptions } } = action.payload
return {
...state,
@ -723,6 +718,18 @@ export const runTabReducer = (state: RunTabState = runTabInitialState, action: A
}
}
case SET_PROXY_ENV_ADDRESS: {
const payload: string = action.payload
return {
...state,
contracts: {
...state.contracts,
proxyKey: payload
}
}
}
default:
return state
}

@ -128,7 +128,8 @@ export interface ContractDropdownProps {
exEnvironment: string,
contracts: {
contractList: ContractList,
deployOptions: DeployOptions,
deployOptions: { [file: string]: { [name: string]: DeployOptions } },
proxyKey: string,
loadType: 'abi' | 'sol' | 'other',
currentFile: string,
currentContract: string,
@ -219,7 +220,7 @@ export interface Modal {
cancelFn: () => void
}
export type DeployMode = 'Deploy with Proxy' | 'Upgrade Proxy'
export type DeployMode = 'Deploy with Proxy' | 'Upgrade with Proxy'
export type DeployOption = {
initializeInputs: string,
@ -238,10 +239,8 @@ export type DeployOption = {
}
}
export interface DeployOptions {
initializeOptions: {
[key: string]: DeployOption
},
options: { title: DeployMode, active: boolean }[],
initializeOptions: DeployOption,
options: { title: DeployMode, active: boolean }[]
}
export interface ContractGUIProps {
@ -255,7 +254,8 @@ export interface ContractGUIProps {
disabled?: boolean,
isDeploy?: boolean,
deployOption?: { title: DeployMode, active: boolean }[],
initializerOptions?: DeployOption
initializerOptions?: DeployOption,
savedProxyAddress?: string
}
export interface MainnetProps {
network: Network,

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

@ -7,7 +7,8 @@ import './remix-ui-settings.css'
import { ethereumVM, generateContractMetadat, personal, textWrapEventAction, useMatomoAnalytics, saveTokenToast, removeTokenToast, saveSwarmSettingsToast, saveIpfsSettingsToast, useAutoCompletion } from './settingsAction'
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 { RemixUiThemeModule, ThemeModule} from '@remix-ui/theme-module'
import { GithubSettings } from './github-settings'
/* eslint-disable-next-line */
export interface RemixUiSettingsProps {
@ -357,9 +358,21 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
return (
<div>
{state.message ? <Toaster message={state.message} /> : null}
{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()}

@ -48,12 +48,12 @@ export const useAutoCompletion = (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
}
}

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

@ -8,7 +8,6 @@ import { createWorkspaceTemplate, getWorkspaces, loadWorkspacePreset, setPlugin
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'
@ -21,20 +20,24 @@ let plugin, dispatch: React.Dispatch<any>
export type UrlParametersType = {
gist: string,
code: string,
url: string
url: string,
address: string
}
const basicWorkspaceInit = async (workspaces, workspaceProvider) => {
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('default_workspace'))
dispatch(setCurrentWorkspace({ name: 'default_workspace', isGitRepo: false }))
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]))
const workspace = workspaces[workspaces.length - 1]
const workspaceName = (workspace || {}).name
workspaceProvider.setWorkspace(workspaceName)
plugin.setWorkspace({ name: workspaceName, isLocalhost: false })
dispatch(setCurrentWorkspace(workspace))
}
}
}
@ -52,18 +55,17 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
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 (window.location.pathname && window.location.pathname !== '/') {
let route = window.location.pathname
if (route.startsWith('/address/0x') && route.length === 51) {
const contractAddress = route.split('/')[2]
} else if (params.address) {
if (params.address.startsWith('0x') && params.address.length === 42) {
const contractAddress = params.address
plugin.call('notification', 'toast', `Looking for contract(s) verified on different networks of Etherscan for contract address ${contractAddress} .....`)
let data
let count = 0
@ -95,7 +97,7 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
foundOnNetworks.push(network.name)
await createWorkspaceTemplate('etherscan-code-sample', 'code-template')
plugin.setWorkspace({ name: 'etherscan-code-sample', isLocalhost: false })
dispatch(setCurrentWorkspace('etherscan-code-sample'))
dispatch(setCurrentWorkspace({ name: 'etherscan-code-sample', isGitRepo: false }))
let filePath
count = count + (Object.keys(data.compilationTargets)).length
for (filePath in data.compilationTargets)
@ -106,24 +108,15 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
} 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('github-code-sample'))
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 if (localStorage.getItem("currentWorkspace")) {
const index = workspaces.findIndex(element => element.name == localStorage.getItem("currentWorkspace"))
if (index !== -1) {
const name = localStorage.getItem("currentWorkspace")
workspaceProvider.setWorkspace(name)
plugin.setWorkspace({ name: name, isLocalhost: false })
dispatch(setCurrentWorkspace({ name: name, isGitRepo: false }))
}
} else {
await basicWorkspaceInit(workspaces, workspaceProvider)
}

@ -1,14 +1,14 @@
import { fileDecoration } from '@remix-ui/file-decorators'
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
@ -126,7 +126,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
@ -247,3 +247,20 @@ export const setFileDecorationSuccess = (items: fileDecoration[]) => {
payload: items
}
}
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) {}
}
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,9 @@ 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 +125,10 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
await restoreBackupZip()
}
const dispatchCloneRepository = async (url: string) => {
await cloneRepository(url)
}
useEffect(() => {
dispatchInitWorkspace()
}, [])
@ -224,7 +230,8 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
dispatchHandleClickFile,
dispatchHandleExpandPath,
dispatchHandleDownloadFiles,
dispatchHandleRestoreBackup
dispatchHandleRestoreBackup,
dispatchCloneRepository
}
return (
<FileSystemContext.Provider value={value}>

@ -9,13 +9,18 @@ interface Action {
export interface BrowserState {
browser: {
currentWorkspace: string,
workspaces: string[],
workspaces: {
name: string;
isGitRepo: boolean;
}[],
files: { [x: string]: Record<string, FileType> },
expandPath: string[]
isRequestingDirectory: boolean,
isSuccessfulDirectory: boolean,
isRequestingWorkspace: boolean,
isSuccessfulWorkspace: boolean,
isRequestingCloning: boolean,
isSuccessfulCloning: boolean,
error: string,
contextMenu: {
registeredMenuItems: action[],
@ -66,6 +71,8 @@ export const browserInitialState: BrowserState = {
isSuccessfulDirectory: false,
isRequestingWorkspace: false,
isSuccessfulWorkspace: false,
isRequestingCloning: false,
isSuccessfulCloning: false,
error: null,
contextMenu: {
registeredMenuItems: [],
@ -109,21 +116,21 @@ export const browserInitialState: BrowserState = {
export const browserReducer = (state = browserInitialState, action: Action) => {
switch (action.type) {
case 'SET_CURRENT_WORKSPACE': {
const payload = action.payload as string
const workspaces = state.browser.workspaces.includes(payload) ? state.browser.workspaces : [...state.browser.workspaces, action.payload]
const payload = action.payload as { name: string; isGitRepo: boolean; }
const workspaces = state.browser.workspaces.find(({ name }) => name === payload.name) ? state.browser.workspaces : [...state.browser.workspaces, action.payload]
return {
...state,
browser: {
...state.browser,
currentWorkspace: payload,
currentWorkspace: payload.name,
workspaces: workspaces.filter(workspace => workspace)
}
}
}
case 'SET_WORKSPACES': {
const payload = action.payload as string[]
const payload = action.payload as { name: string; isGitRepo: boolean; }[]
return {
...state,
@ -421,14 +428,14 @@ export const browserReducer = (state = browserInitialState, action: Action) => {
}
case 'CREATE_WORKSPACE_SUCCESS': {
const payload = action.payload as string
const workspaces = state.browser.workspaces.includes(payload) ? state.browser.workspaces : [...state.browser.workspaces, action.payload]
const payload = action.payload as { name: string; isGitRepo: boolean; }
const workspaces = state.browser.workspaces.find(({ name }) => name === payload.name) ? state.browser.workspaces : [...state.browser.workspaces, action.payload]
return {
...state,
browser: {
...state.browser,
currentWorkspace: payload,
currentWorkspace: payload.name,
workspaces: workspaces.filter(workspace => workspace),
isRequestingWorkspace: false,
isSuccessfulWorkspace: true,
@ -451,14 +458,25 @@ export const browserReducer = (state = browserInitialState, action: Action) => {
case 'RENAME_WORKSPACE': {
const payload = action.payload as { oldName: string, workspaceName: string }
const workspaces = state.browser.workspaces.filter(name => name && (name !== payload.oldName))
let renamedWorkspace
const workspaces = state.browser.workspaces.filter(({ name, isGitRepo }) => {
if (name && (name !== payload.oldName)) {
return true
} else {
renamedWorkspace = {
name: payload.workspaceName,
isGitRepo
}
return false
}
})
return {
...state,
browser: {
...state.browser,
currentWorkspace: payload.workspaceName,
workspaces: [...workspaces, payload.workspaceName],
workspaces: [...workspaces, renamedWorkspace],
expandPath: []
}
}
@ -466,7 +484,7 @@ export const browserReducer = (state = browserInitialState, action: Action) => {
case 'DELETE_WORKSPACE': {
const payload = action.payload as string
const workspaces = state.browser.workspaces.filter(name => name && (name !== payload))
const workspaces = state.browser.workspaces.filter(({ name }) => name && (name !== payload))
return {
...state,
@ -597,6 +615,39 @@ export const browserReducer = (state = browserInitialState, action: Action) => {
}
}
case 'CLONE_REPOSITORY_REQUEST': {
return {
...state,
browser: {
...state.browser,
isRequestingCloning: true,
isSuccessfulCloning: false
}
}
}
case 'CLONE_REPOSITORY_SUCCESS': {
return {
...state,
browser: {
...state.browser,
isRequestingCloning: false,
isSuccessfulCloning: true
}
}
}
case 'CLONE_REPOSITORY_FAILED': {
return {
...state,
browser: {
...state.browser,
isRequestingCloning: false,
isSuccessfulCloning: false
}
}
}
case 'FS_INITIALIZATION_COMPLETED': {
return {
...state,

@ -1,7 +1,9 @@
import React, { useState, useEffect, useRef, useContext } from 'react' // eslint-disable-line
import { Dropdown } from 'react-bootstrap'
import { CustomMenu, CustomToggle } from '@remix-ui/helper'
import { FileExplorer } from './components/file-explorer' // eslint-disable-line
import './css/remix-ui-workspace.css'
import { FileSystemContext } from './contexts'
import './css/remix-ui-workspace.css'
const canUpload = window.File || window.FileReader || window.FileList || window.Blob
@ -9,12 +11,16 @@ export function Workspace () {
const LOCALHOST = ' - connect to localhost - '
const NO_WORKSPACE = ' - none - '
const [currentWorkspace, setCurrentWorkspace] = useState<string>(NO_WORKSPACE)
const [selectedWorkspace, setSelectedWorkspace] = useState<{ name: string, isGitRepo: boolean}>(null)
const [showDropdown, setShowDropdown] = useState<boolean>(false)
const global = useContext(FileSystemContext)
const workspaceRenameInput = useRef()
const workspaceCreateInput = useRef()
const workspaceCreateTemplateInput = useRef()
const cloneUrlRef = useRef<HTMLInputElement>()
useEffect(() => {
setCurrentWorkspace(localStorage.getItem('currentWorkspace') ? localStorage.getItem('currentWorkspace') : '')
resetFocus()
}, [])
@ -30,15 +36,21 @@ export function Workspace () {
}, [global.fs.browser.currentWorkspace, global.fs.localhost.sharedFolder, global.fs.mode])
useEffect(() => {
if (global.fs.browser.currentWorkspace && !global.fs.browser.workspaces.includes(global.fs.browser.currentWorkspace)) {
if (global.fs.browser.currentWorkspace && !global.fs.browser.workspaces.find(({ name }) => name === global.fs.browser.currentWorkspace)) {
if (global.fs.browser.workspaces.length > 0) {
switchWorkspace(global.fs.browser.workspaces[global.fs.browser.workspaces.length - 1])
switchWorkspace(global.fs.browser.workspaces[global.fs.browser.workspaces.length - 1].name)
} else {
switchWorkspace(NO_WORKSPACE)
}
}
}, [global.fs.browser.workspaces])
useEffect(() => {
const workspace = global.fs.browser.workspaces.find(workspace => workspace.name === currentWorkspace)
setSelectedWorkspace(workspace)
}, [currentWorkspace])
const renameCurrentWorkspace = () => {
global.modal('Rename Current Workspace', renameModalMessage(), 'OK', onFinishRenameWorkspace, '')
}
@ -51,6 +63,10 @@ export function Workspace () {
global.modal('Delete Current Workspace', 'Are you sure to delete the current workspace?', 'OK', onFinishDeleteWorkspace, '')
}
const cloneGitRepository = () => {
global.modal('Clone Git Repository', cloneModalMessage(), 'OK', handleTypingUrl, '')
}
const downloadWorkspaces = async () => {
try {
await global.dispatchHandleDownloadFiles()
@ -124,6 +140,20 @@ export function Workspace () {
workspaceCreateInput.current.value = `${workspaceCreateTemplateInput.current.value || 'remixDefault'}_${Date.now()}`
}
const handleTypingUrl = () => {
const url = cloneUrlRef.current.value
if (url) {
global.dispatchCloneRepository(url)
} else {
global.modal('Clone Git Repository', 'Please provide a valid git repository url.', 'OK', () => {}, '')
}
}
const toggleDropdown = (isOpen: boolean) => {
setShowDropdown(isOpen)
}
const createModalMessage = () => {
return (
<>
@ -149,13 +179,21 @@ export function Workspace () {
)
}
const cloneModalMessage = () => {
return (
<>
<input type="text" data-id="modalDialogCustomPromptTextClone" placeholder='Enter git repository url' ref={cloneUrlRef} className="form-control" />
</>
)
}
return (
<div className='remixui_container'>
<div className='remixui_fileexplorer' data-id="remixUIWorkspaceExplorer" onClick={resetFocus}>
<div>
<header>
<div className="mb-2">
<label className="form-check-label" htmlFor="workspacesSelect">
<label className="pl-1 form-check-label" htmlFor="workspacesSelect">
Workspaces
</label>
<span className="remixui_menu">
@ -214,23 +252,53 @@ export function Workspace () {
className='far fa-upload remixui_menuicon'
title='Restore Workspaces Backup'>
</span>
<span
id='cloneGitRepository'
data-id='cloneGitRepository'
onClick={(e) => {
e.stopPropagation()
cloneGitRepository()
}}
className='far fa-clone remixui_menuicon'
title='Clone Git Repository'>
</span>
</span>
<select id="workspacesSelect" value={currentWorkspace} data-id="workspacesSelect" onChange={(e) => switchWorkspace(e.target.value)} className="form-control custom-select">
<Dropdown id="workspacesSelect" data-id="workspacesSelect" onToggle={toggleDropdown} show={showDropdown}>
<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={selectedWorkspace && selectedWorkspace.isGitRepo && !(currentWorkspace === LOCALHOST) ? 'far fa-code-branch' : null}>
{ selectedWorkspace ? selectedWorkspace.name : currentWorkspace === LOCALHOST ? 'localhost' : NO_WORKSPACE }
</Dropdown.Toggle>
<Dropdown.Menu as={CustomMenu} className='w-100 custom-dropdown-items' data-id="custom-dropdown-items">
{
global.fs.browser.workspaces
.map((folder, index) => {
return <option key={index} value={folder}>{folder}</option>
})
global.fs.browser.workspaces.map(({ name, isGitRepo }, index) => (
<Dropdown.Item
key={index}
onClick={() => {
switchWorkspace(name)
}}
data-id={`dropdown-item-${name}`}
>
{ isGitRepo ?
<div className='d-flex justify-content-between'>
<span>{ currentWorkspace === name ? <span>&#10003; { name } </span> : <span className="pl-3">{ name }</span> }</span>
<i className='fas fa-code-branch pt-1'></i>
</div> :
<span>{ currentWorkspace === name ? <span>&#10003; { name } </span> : <span className="pl-3">{ name }</span> }</span>
}
<option value={LOCALHOST}>{currentWorkspace === LOCALHOST ? 'localhost' : LOCALHOST}</option>
{ global.fs.browser.workspaces.length <= 0 && <option value={NO_WORKSPACE}>{NO_WORKSPACE}</option> }
</select>
</Dropdown.Item>
))
}
<Dropdown.Item onClick={() => { switchWorkspace(LOCALHOST) }}>{currentWorkspace === LOCALHOST ? <span>&#10003; localhost </span> : <span className="pl-3"> { LOCALHOST } </span>}</Dropdown.Item>
{ ((global.fs.browser.workspaces.length <= 0) || currentWorkspace === NO_WORKSPACE) && <Dropdown.Item onClick={() => { switchWorkspace(NO_WORKSPACE) }}>{ <span className="pl-3">NO_WORKSPACE</span> }</Dropdown.Item> }
</Dropdown.Menu>
</Dropdown>
</div>
</header>
</div>
<div className='h-100 remixui_fileExplorerTree'>
<div className='h-100 remixui_fileExplorerTree' onFocus={() => { toggleDropdown(false) }}>
<div className='h-100'>
<div className='pl-2 remixui_treeview' data-id='filePanelFileExplorerTree'>
{ global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning ? <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div>
: <div className='pl-2 remixui_treeview' data-id='filePanelFileExplorerTree'>
{ (global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) &&
<FileExplorer
name={currentWorkspace}
@ -264,6 +332,7 @@ export function Workspace () {
/>
}
</div>
}
{
global.fs.localhost.isRequestingLocalhost ? <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div>
: <div className='pl-2 filesystemexplorer remixui_treeview'>

@ -1,6 +1,6 @@
{
"name": "@remix-project/remixd",
"version": "0.6.2",
"version": "0.6.3",
"description": "remix server: allow accessing file system from remix.ethereum.org and start a dev environment (see help section)",
"main": "index.js",
"types": "./index.d.ts",

@ -169,6 +169,9 @@
},
"remix-ui-file-decorators": {
"tags": []
},
"remix-ui-tooltip-popup": {
"tags": []
}
},
"targetDependencies": {

@ -45,7 +45,7 @@
"workspace-schematic": "nx workspace-schematic",
"dep-graph": "nx dep-graph",
"help": "nx help",
"lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remix-ws-templates,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-helper,remix-ui-debugger-ui,remix-ui-workspace,remix-ui-static-analyser,remix-ui-checkbox,remix-ui-settings,remix-core-plugin,remix-ui-renderer,remix-ui-publish-to-storage,remix-ui-solidity-compiler,solidity-unit-testing,remix-ui-plugin-manager,remix-ui-terminal,remix-ui-editor,remix-ui-app,remix-ui-tabs,remix-ui-panel,remix-ui-run-tab,remix-ui-permission-handler,remix-ui-search,remix-ui-file-decorators",
"lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remix-ws-templates,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-helper,remix-ui-debugger-ui,remix-ui-workspace,remix-ui-static-analyser,remix-ui-checkbox,remix-ui-settings,remix-core-plugin,remix-ui-renderer,remix-ui-publish-to-storage,remix-ui-solidity-compiler,solidity-unit-testing,remix-ui-plugin-manager,remix-ui-terminal,remix-ui-editor,remix-ui-app,remix-ui-tabs,remix-ui-panel,remix-ui-run-tab,remix-ui-permission-handler,remix-ui-search,remix-ui-file-decorators,remix-ui-tooltip-popup",
"build:libs": "nx run-many --target=build --parallel=false --with-deps=true --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remix-ws-templates,remixd",
"test:libs": "nx run-many --target=test --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd",
"publish:libs": "yarn run build:libs && lerna publish --skip-git && yarn run bumpVersion:libs",
@ -153,13 +153,13 @@
"@ethersphere/bee-js": "^3.2.0",
"@isomorphic-git/lightning-fs": "^4.4.1",
"@monaco-editor/react": "4.4.5",
"@remixproject/engine": "^0.3.28",
"@remixproject/engine-web": "^0.3.28",
"@remixproject/plugin": "^0.3.28",
"@remixproject/plugin-api": "^0.3.28",
"@remixproject/plugin-utils": "^0.3.28",
"@remixproject/plugin-webview": "^0.3.28",
"@remixproject/plugin-ws": "^0.3.28",
"@remixproject/engine": "^0.3.31",
"@remixproject/engine-web": "^0.3.31",
"@remixproject/plugin": "^0.3.31",
"@remixproject/plugin-api": "^0.3.31",
"@remixproject/plugin-utils": "^0.3.31",
"@remixproject/plugin-webview": "^0.3.31",
"@remixproject/plugin-ws": "^0.3.31",
"@types/nightwatch": "^2.0.9",
"ansi-gray": "^0.1.1",
"async": "^2.6.2",

@ -85,7 +85,8 @@
"@remix-ui/run-tab": ["libs/remix-ui/run-tab/src/index.ts"],
"@remix-ui/permission-handler": [
"libs/remix-ui/permission-handler/src/index.ts"
]
],
"@remix-ui/tooltip-popup": ["libs/remix-ui/tooltip-popup/src/index.ts"]
}
},
"exclude": ["node_modules", "tmp"]

@ -18,6 +18,7 @@
"assets": [
"apps/remix-ide/src/assets",
"apps/remix-ide/src/index.html",
"apps/remix-ide/src/404.html",
"apps/remix-ide/src/favicon.ico"
],
"styles": [],
@ -1250,6 +1251,22 @@
}
}
}
},
"remix-ui-tooltip-popup": {
"root": "libs/remix-ui/tooltip-popup",
"sourceRoot": "libs/remix-ui/tooltip-popup/src",
"projectType": "library",
"architect": {
"lint": {
"builder": "@nrwl/linter:lint",
"options": {
"linter": "eslint",
"config": "libs/remix-ui/tooltip-popup/.eslintrc.json",
"tsConfig": ["libs/remix-ui/tooltip-popup/tsconfig.lib.json"],
"exclude": ["**/node_modules/**"]
}
}
}
}
},
"cli": {

@ -3761,63 +3761,63 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590"
integrity sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==
"@remixproject/engine-web@^0.3.28":
version "0.3.28"
resolved "https://registry.yarnpkg.com/@remixproject/engine-web/-/engine-web-0.3.28.tgz#9a47c979a89764cb96d3f5da0d6802d1ca875c94"
integrity sha512-JTwTuonW+pI7WsSsgGHAzPynGjIdG34VnPrspR9XKBQO4+NX7m3lbNIMUUviyrbqIoGiGtgYd/ry98uLpTHr4g==
"@remixproject/engine-web@^0.3.31":
version "0.3.31"
resolved "https://registry.yarnpkg.com/@remixproject/engine-web/-/engine-web-0.3.31.tgz#42ee5c5d874ef9646090342ca8b430e3fe1046e8"
integrity sha512-GYeUB4Y0NaVc23mSmVByhx6pCvjzfz8iBbJCwaUB3q61uyRQ1wMM0P/47qqwZMukPi7x4bFUpN41C6rKq7ECug==
dependencies:
"@remixproject/engine" "0.3.28"
"@remixproject/plugin-api" "0.3.28"
"@remixproject/plugin-utils" "0.3.28"
"@remixproject/engine" "0.3.31"
"@remixproject/plugin-api" "0.3.31"
"@remixproject/plugin-utils" "0.3.31"
"@remixproject/engine@0.3.28", "@remixproject/engine@^0.3.28":
version "0.3.28"
resolved "https://registry.yarnpkg.com/@remixproject/engine/-/engine-0.3.28.tgz#3656ee029d3cbc22f8ad95bd925176a75057a475"
integrity sha512-27SHaCrG3KrPfMa1MYud4tE9xUIJKITEUsql99fhN7x12lOmgGZFjiqIG/WWjCMmT7OMG3vtLayiZrsYkzHCVw==
"@remixproject/engine@0.3.31", "@remixproject/engine@^0.3.31":
version "0.3.31"
resolved "https://registry.yarnpkg.com/@remixproject/engine/-/engine-0.3.31.tgz#6dcc209dbd993646430335c9454ce1a739d1a45f"
integrity sha512-5VTl9bkeU3fd3IUV8wy7kXHMd3RfnUsf6dlmORM9Np1oMZ9nhdhUsyJO/qXyrvR1PbyJIxPz/qM4+W5MGvOrnQ==
dependencies:
"@remixproject/plugin-api" "0.3.28"
"@remixproject/plugin-utils" "0.3.28"
"@remixproject/plugin-api" "0.3.31"
"@remixproject/plugin-utils" "0.3.31"
"@remixproject/plugin-api@0.3.28", "@remixproject/plugin-api@^0.3.28":
version "0.3.28"
resolved "https://registry.yarnpkg.com/@remixproject/plugin-api/-/plugin-api-0.3.28.tgz#660f068681bc1780284768e4ce1b4f70ea4ba859"
integrity sha512-tOzVFR504037weEkNHQGZsk+Ebxcu/xnBsVCkJaQaBMb+H/i6YevKQmoR1aGcH+JfQxc8eS8LGC65MaV8zmcWg==
"@remixproject/plugin-api@0.3.31", "@remixproject/plugin-api@^0.3.31":
version "0.3.31"
resolved "https://registry.yarnpkg.com/@remixproject/plugin-api/-/plugin-api-0.3.31.tgz#86e7c458c58ff200bd927fd3d642877f4b5a0013"
integrity sha512-LOJRHxORNp7zW8k7//DQz5aZ7eqB7TwhYXrvzqvaryDTvtvJGWrlTHg81hzALynaxZKEWneohxjUxKvGp/eA4g==
dependencies:
"@remixproject/plugin-utils" "0.3.28"
"@remixproject/plugin-utils" "0.3.31"
"@remixproject/plugin-utils@0.3.28", "@remixproject/plugin-utils@^0.3.28":
version "0.3.28"
resolved "https://registry.yarnpkg.com/@remixproject/plugin-utils/-/plugin-utils-0.3.28.tgz#eefd3b603f223cf6fba9b914ef3c9a62bd8435b6"
integrity sha512-5CayTgMqRiOguanTy6kpuRgCmjEFLUu2K4Rs7Zmt+GOzHucmwkDxYQO+3hFv0Jz/M/6yC5wkKYTx8vfPLLdEBQ==
"@remixproject/plugin-utils@0.3.31", "@remixproject/plugin-utils@^0.3.31":
version "0.3.31"
resolved "https://registry.yarnpkg.com/@remixproject/plugin-utils/-/plugin-utils-0.3.31.tgz#80771e00c1a1b776432abb17b1f4b2e25600d4f6"
integrity sha512-OOAjoSd+ErBMrcNQEh80NU3BjJ9fHXuftRfy5Ul9aGXN3b1LJSNVvfrG+FxX6lpyaAK5JBj+aB9pgFozgb2wlw==
dependencies:
tslib "2.0.1"
"@remixproject/plugin-webview@^0.3.28":
version "0.3.28"
resolved "https://registry.yarnpkg.com/@remixproject/plugin-webview/-/plugin-webview-0.3.28.tgz#ce03ad0801e0fd1e5bce59565b77b6698ad07596"
integrity sha512-DodSkN0vMSo2DEEoBOWyKxC/ElvBA826vhX+JVGXA8HGS45aavplZIN967hsc+SdzjV1UZfSNKEHttLGwM1BIw==
"@remixproject/plugin-webview@^0.3.31":
version "0.3.31"
resolved "https://registry.yarnpkg.com/@remixproject/plugin-webview/-/plugin-webview-0.3.31.tgz#e5cce7d0089439b35aee4ab2a724add1e5d36b40"
integrity sha512-8yoKwIkRi9S+rqvFShNt01FfXX0H/Fijn7UkWFWJ/V6ULcw2cw9ViCz8cYZLhNUpxqezyu/LKDQL9g1TbJJoYw==
dependencies:
"@remixproject/plugin" "0.3.28"
"@remixproject/plugin-api" "0.3.28"
"@remixproject/plugin-utils" "0.3.28"
"@remixproject/plugin" "0.3.31"
"@remixproject/plugin-api" "0.3.31"
"@remixproject/plugin-utils" "0.3.31"
axios "^0.21.1"
"@remixproject/plugin-ws@^0.3.28":
version "0.3.28"
resolved "https://registry.yarnpkg.com/@remixproject/plugin-ws/-/plugin-ws-0.3.28.tgz#4f6022b1c8ef6356299c545f6164bce48aac60fe"
integrity sha512-i3He9t4qvcBQxzshFx66D6drqLlySmV7Cb+NeYtMOYlWxQSJtUakp/Px1Tl3IDFQXDfpXxvDvYhn2w0AWPBOqw==
"@remixproject/plugin-ws@^0.3.31":
version "0.3.31"
resolved "https://registry.yarnpkg.com/@remixproject/plugin-ws/-/plugin-ws-0.3.31.tgz#a2bb415cc32a5b036b9b3759520088b8ceaa0d5a"
integrity sha512-Z4G6vkGxxCP+ibGNHAvNaFjYE4hbsazOEL50pMnda6LZNci9akMSiI/1MIZscVSMU8l2sYOoNUsWvLfLkIkYKQ==
dependencies:
"@remixproject/plugin" "0.3.28"
"@remixproject/plugin-api" "0.3.28"
"@remixproject/plugin-utils" "0.3.28"
"@remixproject/plugin" "0.3.31"
"@remixproject/plugin-api" "0.3.31"
"@remixproject/plugin-utils" "0.3.31"
"@remixproject/plugin@0.3.28", "@remixproject/plugin@^0.3.28":
version "0.3.28"
resolved "https://registry.yarnpkg.com/@remixproject/plugin/-/plugin-0.3.28.tgz#3e40de6d0441e6b47ca17c904e57562cb285a4d3"
integrity sha512-sw+cow3GKAcEacXLvdJwtHHIphPty4KRMZ6tcQHdtx/iCKTMNdw2hribKPnvTO+6mXE+4oAKCrFqeTdVp/rd1w==
"@remixproject/plugin@0.3.31", "@remixproject/plugin@^0.3.31":
version "0.3.31"
resolved "https://registry.yarnpkg.com/@remixproject/plugin/-/plugin-0.3.31.tgz#b6c6b58d2c7964e37024eeca4819c70ece1f3953"
integrity sha512-9ntMU9CzStloahm/wXt4V8n64ERgJzY5nG0bzQfjnI12knrdTmUo+LC42M2xaTBDDP9CzMPdqClg7XhhRLzohA==
dependencies:
"@remixproject/plugin-api" "0.3.28"
"@remixproject/plugin-utils" "0.3.28"
"@remixproject/plugin-api" "0.3.31"
"@remixproject/plugin-utils" "0.3.31"
events "3.2.0"
"@restart/context@^2.1.4":

Loading…
Cancel
Save