diff --git a/.circleci/config.yml b/.circleci/config.yml index 4d422599c2..7fcd0cd3d1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -51,6 +51,7 @@ jobs: - checkout - run: npm install - run: npm run build:libs + - run: cd dist/libs/remix-tests && npm install - run: npm run test:libs remix-ide-chrome-1: diff --git a/.gitignore b/.gitignore index 426832614f..5d71c08e57 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ soljson.js *.launch .settings/ *.sublime-workspace +.vscode/ # IDE - VSCode .vscode/* diff --git a/apps/debugger/src/app/debugger-api.ts b/apps/debugger/src/app/debugger-api.ts index 73c49b32e9..41b2cdb8e4 100644 --- a/apps/debugger/src/app/debugger-api.ts +++ b/apps/debugger/src/app/debugger-api.ts @@ -4,6 +4,9 @@ import { CompilationOutput, Sources } from '@remix-ui/debugger-ui' import type { CompilationResult } from '@remix-project/remix-solidity-ts' export const DebuggerApiMixin = (Base) => class extends Base { + + initialWeb3 + initDebuggerApi () { this.debugHash = null @@ -16,6 +19,8 @@ export const DebuggerApiMixin = (Base) => class extends Base { } } this._web3 = new Web3(this.web3Provider) + // this._web3 can be overwritten and reset to initial value in 'debug' method + this.initialWeb3 = this._web3 remixDebug.init.extendWeb3(this._web3) this.offsetToLineColumnConverter = { @@ -123,7 +128,9 @@ export const DebuggerApiMixin = (Base) => class extends Base { debug (hash, web3?) { this.debugHash = hash - if (web3) remixDebug.init.extendWeb3(web3) + if (web3) this._web3 = web3 + else this._web3 = this.initialWeb3 + remixDebug.init.extendWeb3(this._web3) if (this.onDebugRequestedListener) this.onDebugRequestedListener(hash, web3) } diff --git a/apps/remix-ide-e2e/src/commands/addFile.ts b/apps/remix-ide-e2e/src/commands/addFile.ts index aa1f8d7f6e..8fc6b1940b 100644 --- a/apps/remix-ide-e2e/src/commands/addFile.ts +++ b/apps/remix-ide-e2e/src/commands/addFile.ts @@ -17,7 +17,7 @@ function addFile (browser: NightwatchBrowser, name: string, content: NightwatchC browser.clickLaunchIcon('udapp') .clickLaunchIcon('filePanel') .click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory - .click('.newFile') + .click('[data-id="fileExplorerNewFilecreateNewFile"]') .waitForElementContainsText('*[data-id$="/blank"]', '', 60000) .sendKeys('*[data-id$="/blank"] .remixui_items', name) .sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER) diff --git a/apps/remix-ide-e2e/src/commands/executeScript.ts b/apps/remix-ide-e2e/src/commands/executeScript.ts index 77649a2664..dbb93e66db 100644 --- a/apps/remix-ide-e2e/src/commands/executeScript.ts +++ b/apps/remix-ide-e2e/src/commands/executeScript.ts @@ -6,6 +6,7 @@ class ExecuteScript extends EventEmitter { this.api .clearEditableContent('*[data-id="terminalCliInput"]') .click('*[data-id="terminalCli"]') + .setValue('*[data-id="terminalCliInput"]', [this.api.Keys.CONTROL, 'a', this.api.Keys.DELETE]) .sendKeys('*[data-id="terminalCliInput"]', script) .sendKeys('*[data-id="terminalCliInput"]', this.api.Keys.ENTER) .sendKeys('*[data-id="terminalCliInput"]', this.api.Keys.ENTER) diff --git a/apps/remix-ide-e2e/src/commands/journalLastChildIncludes.ts b/apps/remix-ide-e2e/src/commands/journalLastChildIncludes.ts index e99f611a79..7e63d703b6 100644 --- a/apps/remix-ide-e2e/src/commands/journalLastChildIncludes.ts +++ b/apps/remix-ide-e2e/src/commands/journalLastChildIncludes.ts @@ -7,11 +7,12 @@ import EventEmitter from 'events' class JournalLastChildIncludes extends EventEmitter { command (this: NightwatchBrowser, val: string): NightwatchBrowser { this.api - .waitForElementVisible('*[data-id="terminalJournal"] > div:last-child', 10000) - .getText('*[data-id="terminalJournal"] > div:last-child', (result) => { + .waitForElementVisible('*[data-id="terminalJournal"]', 10000) + .pause(1000) + .getText('*[data-id="terminalJournal"]', (result) => { console.log('JournalLastChildIncludes', result.value) if (typeof result.value === 'string' && result.value.indexOf(val) === -1) return this.api.assert.fail(`wait for ${val} in ${result.value}`) - else this.api.assert.ok(true, `<*[data-id="terminalJournal"] > div:last-child> contains ${val}.`) + else this.api.assert.ok(true, `<*[data-id="terminalJournal"]> contains ${val}.`) this.emit('complete') }) return this diff --git a/apps/remix-ide-e2e/src/commands/testFunction.ts b/apps/remix-ide-e2e/src/commands/testFunction.ts index e16d613718..371fec9568 100644 --- a/apps/remix-ide-e2e/src/commands/testFunction.ts +++ b/apps/remix-ide-e2e/src/commands/testFunction.ts @@ -24,8 +24,8 @@ class TestFunction extends EventEmitter { .perform((done) => { browser.waitForElementVisible(`[data-id="block_tx${txHash}"]`, 60000) .click(`[data-id="block_tx${txHash}"]`) + .pause(3000) .waitForElementVisible(`*[data-id="txLoggerTable${txHash}"]`, 60000) - .pause(10000) // fetch and format transaction logs as key => pair object .elements('css selector', `*[data-shared="key_${txHash}"]`, (res) => { Array.isArray(res.value) && res.value.forEach(function (jsonWebElement) { @@ -58,7 +58,6 @@ class TestFunction extends EventEmitter { .perform(() => { Object.keys(expectedValue).forEach(key => { let equal = false - try { const receivedValue = JSON.parse(logs[key]) diff --git a/apps/remix-ide-e2e/src/commands/verifyContracts.ts b/apps/remix-ide-e2e/src/commands/verifyContracts.ts index e7dbf4a591..aef1b06217 100644 --- a/apps/remix-ide-e2e/src/commands/verifyContracts.ts +++ b/apps/remix-ide-e2e/src/commands/verifyContracts.ts @@ -17,6 +17,7 @@ function verifyContracts (browser: NightwatchBrowser, compiledContractNames: str browser .clickLaunchIcon('solidity') .pause(opts.wait) + .pause(5000) .waitForElementPresent('*[data-id="compiledContracts"] option', 60000) .perform((done) => { if (opts.version) { diff --git a/apps/remix-ide-e2e/src/tests/ballot.test.ts b/apps/remix-ide-e2e/src/tests/ballot.test.ts index 84e768f420..702de52e10 100644 --- a/apps/remix-ide-e2e/src/tests/ballot.test.ts +++ b/apps/remix-ide-e2e/src/tests/ballot.test.ts @@ -81,8 +81,16 @@ module.exports = { 'Deploy and use Ballot using external web3': function (browser: NightwatchBrowser) { browser - .click('*[data-id="settingsWeb3Mode"]') + .click('option[value="web3"]') + .pause(5000) .modalFooterOKClick() + .execute(function () { + const env: any = document.getElementById('selectExEnvOptions') + return env.value + }, [], function (result) { + console.log({ result }) + browser.assert.ok(result.value === 'web3', 'Web3 Provider not selected') + }) .clickLaunchIcon('solidity') .testContracts('Untitled.sol', sources[0]['Untitled.sol'], ['Ballot']) .clickLaunchIcon('udapp') diff --git a/apps/remix-ide-e2e/src/tests/compiler_api.test.ts b/apps/remix-ide-e2e/src/tests/compiler_api.test.ts index 4128a057bd..cc81e5ace4 100644 --- a/apps/remix-ide-e2e/src/tests/compiler_api.test.ts +++ b/apps/remix-ide-e2e/src/tests/compiler_api.test.ts @@ -21,7 +21,7 @@ module.exports = { browser .addFile('test_jsCompile.js', { content: jsCompile }) .executeScript('remix.exeCurrent()') - .waitForElementContainsText('*[data-id="terminalJournal"]', '"languageversion": "0.6.8+commit.0bbfe453"', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', '"languageversion":"0.6.8+commit.0bbfe453"', 60000) .click('*[data-id="terminalClearConsole"]') }, diff --git a/apps/remix-ide-e2e/src/tests/debugger.spec.ts b/apps/remix-ide-e2e/src/tests/debugger.spec.ts index f069bce1a2..7104fef041 100644 --- a/apps/remix-ide-e2e/src/tests/debugger.spec.ts +++ b/apps/remix-ide-e2e/src/tests/debugger.spec.ts @@ -187,7 +187,8 @@ module.exports = { browser .addFile('test_jsGetTrace.js', { content: jsGetTrace }) .executeScript('remix.exeCurrent()') - .waitForElementContainsText('*[data-id="terminalJournal"]', 'result { "gas": "0x575f", "return": "0x0000000000000000000000000000000000000000000000000000000000000000", "structLogs":', 60000) + .pause(1000) + .waitForElementContainsText('*[data-id="terminalJournal"]', '{"gas":"0x575f","return":"0x0000000000000000000000000000000000000000000000000000000000000000","structLogs":', 60000) }, 'Should call the debugger api: debug': function (browser: NightwatchBrowser) { diff --git a/apps/remix-ide-e2e/src/tests/defaultLayout.test.ts b/apps/remix-ide-e2e/src/tests/defaultLayout.test.ts index 94bc20207d..c8133c4c20 100644 --- a/apps/remix-ide-e2e/src/tests/defaultLayout.test.ts +++ b/apps/remix-ide-e2e/src/tests/defaultLayout.test.ts @@ -50,11 +50,11 @@ module.exports = { 'Toggles Terminal': function (browser: NightwatchBrowser) { browser.waitForElementVisible('div[data-id="terminalContainer"]') - .assert.visible('div[data-id="terminalContainerDisplay"]') + .assert.elementPresent('div[data-id="terminalContainerDisplay"]') .click('i[data-id="terminalToggleIcon"]') .checkElementStyle('div[data-id="terminalToggleMenu"]', 'height', '35px') .click('i[data-id="terminalToggleIcon"]') - .assert.visible('div[data-id="terminalContainerDisplay"]') + .assert.elementPresent('div[data-id="terminalContainerDisplay"]') }, 'Switch Tabs using tabs icon': function (browser: NightwatchBrowser) { diff --git a/apps/remix-ide-e2e/src/tests/editor.spec.ts b/apps/remix-ide-e2e/src/tests/editor.spec.ts index 83d4f23f8f..721b4c7741 100644 --- a/apps/remix-ide-e2e/src/tests/editor.spec.ts +++ b/apps/remix-ide-e2e/src/tests/editor.spec.ts @@ -34,8 +34,8 @@ module.exports = { browser.waitForElementVisible('*[data-id="editorInput"]') .waitForElementVisible('*[class="ace_content"]') .click('*[class="ace_content"]') + .editorScroll('down', 27) // scroll down to line 27 and add the error word .sendKeys('*[class="ace_text-input"]', 'error') - .pause(2000) .waitForElementVisible('.ace_error', 120000) .checkAnnotations('error', 28) .clickLaunchIcon('udapp') @@ -92,9 +92,13 @@ module.exports = { .openFile('sourcehighlight.js') .executeScript('remix.exeCurrent()') .editorScroll('down', 60) + .pause(1000) .waitForElementPresent('.highlightLine32', 60000) + .pause(1000) .checkElementStyle('.highlightLine32', 'background-color', 'rgb(8, 108, 181)') + .pause(1000) .waitForElementPresent('.highlightLine40', 60000) + .pause(1000) .checkElementStyle('.highlightLine40', 'background-color', 'rgb(8, 108, 181)') .waitForElementPresent('.highlightLine50', 60000) .checkElementStyle('.highlightLine50', 'background-color', 'rgb(8, 108, 181)') diff --git a/apps/remix-ide-e2e/src/tests/fileManager_api.spec.ts b/apps/remix-ide-e2e/src/tests/fileManager_api.spec.ts index 26a49d9beb..94d1e6378a 100644 --- a/apps/remix-ide-e2e/src/tests/fileManager_api.spec.ts +++ b/apps/remix-ide-e2e/src/tests/fileManager_api.spec.ts @@ -11,6 +11,7 @@ module.exports = { browser .addFile('file.js', { content: executeFile }) .executeScript('remix.exeCurrent()') + .pause(1000) .waitForElementContainsText('*[data-id="terminalJournal"]', 'file.js', 60000) }, @@ -72,7 +73,8 @@ module.exports = { browser .addFile('readdirFile.js', { content: executeReaddir }) .executeScript('remix.exeCurrent()') - .waitForElementContainsText('*[data-id="terminalJournal"]', 'Test_Folder isDirectory true', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', 'Test_Folder isDirectory', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', 'true', 5000) }, 'Should execute `remove` api from file manager external api': function (browser: NightwatchBrowser) { @@ -175,8 +177,8 @@ const executeMkdir = ` const executeReaddir = ` const run = async () => { const result = await remix.call('fileManager', 'readdir', '/') - - console.log('Test_Folder isDirectory ', result["Test_Folder"].isDirectory) + const output = result["Test_Folder"].isDirectory + console.log('Test_Folder isDirectory ', output) } run() diff --git a/apps/remix-ide-e2e/src/tests/libraryDeployment.test.ts b/apps/remix-ide-e2e/src/tests/libraryDeployment.test.ts index 0a759aa332..9293775733 100644 --- a/apps/remix-ide-e2e/src/tests/libraryDeployment.test.ts +++ b/apps/remix-ide-e2e/src/tests/libraryDeployment.test.ts @@ -77,6 +77,7 @@ function checkDeployShouldFail (browser: NightwatchBrowser, callback: VoidFuncti .clickLaunchIcon('udapp') .selectContract('test') // deploy lib .createContract('') + .pause(2000) .getText('div[class^="terminal"]', (value) => { console.log('value: ', value) }) diff --git a/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts b/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts index f834a91a1f..b9dcccf13b 100644 --- a/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts +++ b/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts @@ -164,6 +164,8 @@ module.exports = { .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') // eslint-disable-next-line dot-notation .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_new' }) + .pause(5000) + .waitForElementPresent('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') .click('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') .click('*[data-id="workspacesSelect"] option[value="workspace_new"]') // end of creating @@ -181,6 +183,7 @@ module.exports = { .clickLaunchIcon('solidityUnitTesting') .pause(2000) .verify.attributeEquals('*[data-id="uiPathInput"]', 'value', 'tests') + .pause(2000) .scrollAndClick('#runTestsTabRunAction') .waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000) .waitForElementPresent('#solidityUnittestsOutput div[class^="testPass"]', 60000) @@ -201,13 +204,13 @@ module.exports = { .waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000) .waitForElementPresent('#solidityUnittestsOutput div[class^="testPass"]', 60000) .waitForElementContainsText('#solidityUnittestsOutput', 'tests/hhLogs_test.sol', 60000) - .assert.containsText('#journal > div:nth-child(3) > span > div', 'Before all:') - .assert.containsText('#journal > div:nth-child(3) > span > div', 'Inside beforeAll') - .assert.containsText('#journal > div:nth-child(4) > span > div', 'Check sender:') - .assert.containsText('#journal > div:nth-child(4) > span > div', 'msg.sender is 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4') - .assert.containsText('#journal > div:nth-child(5) > span > div', 'Check int logs:') - .assert.containsText('#journal > div:nth-child(5) > span > div', '10 20') - .assert.containsText('#journal > div:nth-child(5) > span > div', 'Number is 25') + .assert.containsText('#journal > div:nth-child(2) > span', 'Before all:') + .assert.containsText('#journal > div:nth-child(2) > span', 'Inside beforeAll') + .assert.containsText('#journal > div:nth-child(3) > span', 'Check sender:') + .assert.containsText('#journal > div:nth-child(3) > span', 'msg.sender is 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4') + .assert.containsText('#journal > div:nth-child(4) > span', 'Check int logs:') + .assert.containsText('#journal > div:nth-child(4) > span', '10 20') + .assert.containsText('#journal > div:nth-child(4) > span', 'Number is 25') .openFile('tests/hhLogs_test.sol') .removeFile('tests/hhLogs_test.sol', 'workspace_new') }, @@ -223,13 +226,13 @@ module.exports = { .pause(2000) .waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000) .waitForElementContainsText('#solidityUnittestsOutput', 'tests/ballotFailedLog_test.sol', 60000) - .assert.containsText('#journal > div:nth-child(6) > span > div', 'Check winning proposal:') - .assert.containsText('#journal > div:nth-child(6) > span > div', 'Inside checkWinningProposal') + .assert.containsText('#journal > div:nth-child(5) > span', 'Check winning proposal:') + .assert.containsText('#journal > div:nth-child(5) > span', 'Inside checkWinningProposal') .openFile('tests/ballotFailedLog_test.sol') .removeFile('tests/ballotFailedLog_test.sol', 'workspace_new') }, - 'Debug failed test using debugger': function (browser: NightwatchBrowser) { + 'Debug tests using debugger': function (browser: NightwatchBrowser) { browser .waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]') .addFile('tests/ballotFailedDebug_test.sol', sources[0]['tests/ballotFailedDebug_test.sol']) @@ -239,20 +242,50 @@ module.exports = { .click('#runTestsTabRunAction') .waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000) .waitForElementContainsText('#solidityUnittestsOutput', 'tests/ballotFailedDebug_test.sol', 60000) - .waitForElementContainsText('#solidityUnittestsOutput', '✘ Check winning proposal', 60000) + .waitForElementContainsText('#solidityUnittestsOutput', '✘ Check winning proposal failed', 60000) + .waitForElementContainsText('#solidityUnittestsOutput', '✓ Check winning proposal passed', 60000) + .waitForElementContainsText('#solidityUnittestsOutput', '✘ Check winning proposal again', 60000) .waitForElementContainsText('#solidityUnittestsOutput', '✓ Check winnin proposal with return value', 60000) - .click('.fa-bug') + .click('#Check_winning_proposal_failed') .waitForElementContainsText('*[data-id="sidePanelSwapitTitle"]', 'DEBUGGER', 60000) - .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposal()', 60000) + .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalFailed()', 60000) .click('*[data-id="dropdownPanelSolidityLocals"]') .waitForElementContainsText('*[data-id="solidityLocals"]', 'no locals', 60000) // eslint-disable-next-line dot-notation - .execute(function () { document.getElementById('slider')['value'] = '235' }) // It only moves slider to 235 but vm traces are not updated + .execute(function () { document.getElementById('slider')['value'] = '315' }) // It only moves slider to 315 but vm traces are not updated .setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW)) - .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposal()', 60000) + .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalFailed()', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'vote(proposal)', 60000) - .pause(2000) + .pause(1000) .checkVariableDebug('soliditylocals', locals) + .clickLaunchIcon('solidityUnitTesting') + .scrollAndClick('#Check_winning_proposal_passed') + .waitForElementContainsText('*[data-id="sidePanelSwapitTitle"]', 'DEBUGGER', 60000) + .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalPassed()', 60000) + // eslint-disable-next-line dot-notation + .execute(function () { document.getElementById('slider')['value'] = '1450' }) + .setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW)) + .waitForElementContainsText('*[data-id="functionPanel"]', 'equal(a, b, message)', 60000) + .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalPassed()', 60000) + .pause(1000) + .clickLaunchIcon('solidityUnitTesting') + .scrollAndClick('#Check_winning_proposal_again') + .waitForElementContainsText('*[data-id="sidePanelSwapitTitle"]', 'DEBUGGER', 60000) + .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalAgain()', 60000) + // eslint-disable-next-line dot-notation + .execute(function () { document.getElementById('slider')['value'] = '1150' }) + .setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW)) + .waitForElementContainsText('*[data-id="functionPanel"]', 'equal(a, b, message)', 60000) + .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalAgain()', 60000) + .pause(1000) + .clickLaunchIcon('solidityUnitTesting') + .scrollAndClick('#Check_winnin_proposal_with_return_value') + .waitForElementContainsText('*[data-id="sidePanelSwapitTitle"]', 'DEBUGGER', 60000) + .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinninProposalWithReturnValue()', 60000) + // eslint-disable-next-line dot-notation + .execute(function () { document.getElementById('slider')['value'] = '320' }) + .setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW)) + .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinninProposalWithReturnValue()', 60000) .clickLaunchIcon('filePanel') .pause(2000) .openFile('tests/ballotFailedDebug_test.sol') @@ -268,6 +301,7 @@ module.exports = { .scrollAndClick('[data-id="pluginManagerComponentDeactivateButtonsolidityUnitTesting"]') .pause(2000) .scrollAndClick('[data-id="pluginManagerComponentActivateButtonsolidityUnitTesting"]') + .pause(5000) .clickLaunchIcon('solidityUnitTesting') .scrollAndClick('#runTestsTabRunAction') .waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000) @@ -457,7 +491,7 @@ const sources = [ }, 'tests/deployError_test.sol': { content: ` - pragma solidity ^0.7.0; + pragma solidity ^0.8.0; contract failingDeploy { constructor() { @@ -468,7 +502,7 @@ const sources = [ }, 'tests/methodFailure_test.sol': { content: ` - pragma solidity ^0.7.0; + pragma solidity ^0.8.0; contract methodfailure { function add(uint a, uint b) public { @@ -495,11 +529,20 @@ const sources = [ ballotToTest = new Ballot(proposalNames); } - function checkWinningProposal () public { - ballotToTest.vote(1); // This will revert the transaction + function checkWinningProposalFailed () public { + ballotToTest.vote(1); + Assert.equal(ballotToTest.winningProposal(), uint(0), "proposal at index 0 should be the winning proposal"); + } + + function checkWinningProposalPassed () public { + ballotToTest.vote(0); Assert.equal(ballotToTest.winningProposal(), uint(0), "proposal at index 0 should be the winning proposal"); } + function checkWinningProposalAgain () public { + Assert.equal(ballotToTest.winningProposal(), uint(1), "proposal at index 0 should be the winning proposal"); + } + function checkWinninProposalWithReturnValue () public view returns (bool) { return ballotToTest.winningProposal() == 0; } diff --git a/apps/remix-ide-e2e/src/tests/terminal.test.ts b/apps/remix-ide-e2e/src/tests/terminal.test.ts index 2f42f1717e..ab9660fbeb 100644 --- a/apps/remix-ide-e2e/src/tests/terminal.test.ts +++ b/apps/remix-ide-e2e/src/tests/terminal.test.ts @@ -11,6 +11,7 @@ module.exports = { browser .waitForElementVisible('*[data-id="terminalCli"]', 10000) .executeScript('console.log(1 + 1)') + .pause(2000) .waitForElementContainsText('*[data-id="terminalJournal"]', '2', 60000) }, @@ -30,24 +31,14 @@ module.exports = { .assert.visible('*[data-id="autoCompletePopUpAutoCompleteItem"]') }, - 'Should execute remix.help() command': function (browser: NightwatchBrowser) { - browser - .waitForElementVisible('*[data-id="terminalCli"]') - .executeScript('remix.help()') - .waitForElementContainsText('*[data-id="terminalJournal"]', 'remix.loadgist(id)', 60000) - .waitForElementContainsText('*[data-id="terminalJournal"]', 'remix.loadurl(url)', 60000) - .waitForElementContainsText('*[data-id="terminalJournal"]', 'remix.execute(filepath)', 60000) - .waitForElementContainsText('*[data-id="terminalJournal"]', 'remix.exeCurrent()', 60000) - .waitForElementContainsText('*[data-id="terminalJournal"]', 'remix.help()', 60000) - }, - 'Async/Await Script': function (browser: NightwatchBrowser) { browser .addFile('asyncAwait.js', { content: asyncAwait }) .openFile('asyncAwait.js') - .executeScript('remix.execute(\'asyncAwait.js\')') + .executeScript('remix.execute("asyncAwait.js")') .waitForElementContainsText('*[data-id="terminalJournal"]', 'Waiting Promise', 60000) - .waitForElementContainsText('*[data-id="terminalJournal"]', 'result - Promise Resolved', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', 'result - ', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', 'Promise Resolved', 60000) }, 'Call Remix File Manager from a script': function (browser: NightwatchBrowser) { @@ -67,14 +58,14 @@ module.exports = { 'Call web3.eth.getAccounts() using Web3 Provider': function (browser: NightwatchBrowser) { browser - .click('*[data-id="terminalClearConsole"]') // clear the terminal + .click('*[data-id="terminalClearConsole"]') // clear the terminal .clickLaunchIcon('udapp') .click('*[data-id="settingsWeb3Mode"]') .modalFooterOKClick() .executeScript('web3.eth.getAccounts()') - .waitForElementContainsText('*[data-id="terminalJournal"]', '[ "', 60000) // we check if an array is present, don't need to check for the content - .waitForElementContainsText('*[data-id="terminalJournal"]', '" ]', 60000) - .waitForElementContainsText('*[data-id="terminalJournal"]', '", "', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', '["', 60000) // we check if an array is present, don't need to check for the content + .waitForElementContainsText('*[data-id="terminalJournal"]', '"]', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', '","', 60000) }, 'Call Remix File Resolver (external URL) from a script': function (browser: NightwatchBrowser) { @@ -124,15 +115,18 @@ module.exports = { .clickLaunchIcon('solidity') .click('*[data-id="compilerContainerCompileBtn"]') // compile Owner .executeScript('remix.execute(\'deployWithEthersJs.js\')') - .waitForElementContainsText('*[data-id="terminalJournal"]', 'Contract Address: 0xd9145CCE52D386f254917e481eB44e9943F39138', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', 'Contract Address:', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', '0xd9145CCE52D386f254917e481eB44e9943F39138', 60000) .waitForElementContainsText('*[data-id="terminalJournal"]', 'Deployment successful.', 60000) .addAtAddressInstance('0xd9145CCE52D386f254917e481eB44e9943F39138', true, true) .click('*[data-id="terminalClearConsole"]') // clear the terminal .waitForElementPresent('*[data-id="universalDappUiContractActionWrapper"]', 60000) .click('*[data-id="universalDappUiTitleExpander"]') .clickFunction('changeOwner - transact (not payable)', { types: 'address newOwner', values: '0xd9145CCE52D386f254917e481eB44e9943F39138' }) // execute the "changeOwner" function - .waitForElementContainsText('*[data-id="terminalJournal"]', 'previousOwner0x5B38Da6a701c568545dCfcB03FcB875f56beddC4', 60000) // check that the script is logging the event - .waitForElementContainsText('*[data-id="terminalJournal"]', 'newOwner0xd9145CCE52D386f254917e481eB44e9943F39138', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', 'previousOwner', 60000) // check that the script is logging the event + .waitForElementContainsText('*[data-id="terminalJournal"]', '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4', 60000) // check that the script is logging the event + .waitForElementContainsText('*[data-id="terminalJournal"]', 'newOwner', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', '0xd9145CCE52D386f254917e481eB44e9943F39138', 60000) .end() } } diff --git a/apps/remix-ide-e2e/src/tests/transactionExecution.spec.ts b/apps/remix-ide-e2e/src/tests/transactionExecution.spec.ts index 7dfbd968e9..55f2eadacb 100644 --- a/apps/remix-ide-e2e/src/tests/transactionExecution.spec.ts +++ b/apps/remix-ide-e2e/src/tests/transactionExecution.spec.ts @@ -99,7 +99,7 @@ module.exports = { 'decoded output': { 0: 'uint256: _uret 2343242', 1: 'int256: _iret -4324324', - 2: 'string: _strret string _ string _ string _ string _ string _ string _ string _ string _ string _ string _' + 2: 'string: _strret string _ string _ string _ string _ string _ string _ string _ string _ string _ string _' } }) .pause(500) @@ -147,12 +147,13 @@ module.exports = { .waitForElementPresent('.instance:nth-of-type(3)') .click('.instance:nth-of-type(3) > div > button') .clickFunction('g - transact (not payable)') + .pause(5000) .journalLastChildIncludes('Error provided by the contract:') .journalLastChildIncludes('CustomError : error description') .journalLastChildIncludes('Parameters:') - .journalLastChildIncludes('"value": "2",') - .journalLastChildIncludes('"value": "3",') - .journalLastChildIncludes('"value": "error_string_2",') + .journalLastChildIncludes('"value": "2"') + .journalLastChildIncludes('"value": "3"') + .journalLastChildIncludes('"value": "error_string_2"') .journalLastChildIncludes('"documentation": "param1"') .journalLastChildIncludes('"documentation": "param2"') .journalLastChildIncludes('"documentation": "param3"') @@ -170,9 +171,9 @@ module.exports = { .journalLastChildIncludes('Error provided by the contract:') .journalLastChildIncludes('CustomError : error description') .journalLastChildIncludes('Parameters:') - .journalLastChildIncludes('"value": "2",') - .journalLastChildIncludes('"value": "3",') - .journalLastChildIncludes('"value": "error_string_2",') + .journalLastChildIncludes('"value": "2"') + .journalLastChildIncludes('"value": "3"') + .journalLastChildIncludes('"value": "error_string_2"') .journalLastChildIncludes('"documentation": "param1"') .journalLastChildIncludes('"documentation": "param2"') .journalLastChildIncludes('"documentation": "param3"') diff --git a/apps/remix-ide-e2e/src/tests/workspace.test.ts b/apps/remix-ide-e2e/src/tests/workspace.test.ts index bb6ab325df..204b8e7c31 100644 --- a/apps/remix-ide-e2e/src/tests/workspace.test.ts +++ b/apps/remix-ide-e2e/src/tests/workspace.test.ts @@ -35,19 +35,28 @@ module.exports = { .clickLaunchIcon('filePanel') .click('*[data-id="workspaceCreate"]') // create workspace_name .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') + .waitForElementVisible('[data-id="workspacesModalDialogModalDialogModalFooter-react"] > span') // eslint-disable-next-line dot-notation .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_name' }) - .click('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') + .pause(1000) + .click('span[data-id="workspacesModalDialog-modal-footer-ok-react"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]') + .pause(1000) .addFile('test.sol', { content: 'test' }) .waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]') .click('*[data-id="workspaceCreate"]') // create workspace_name_1 .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') // eslint-disable-next-line dot-notation .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_name_1' }) - .click('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') + .waitForElementVisible('span[data-id="workspacesModalDialog-modal-footer-ok-react"]') + // eslint-disable-next-line dot-notation + .execute(function () { document.querySelector('span[data-id="workspacesModalDialog-modal-footer-ok-react"]') }) + .pause(2000) + .click('span[data-id="workspacesModalDialog-modal-footer-ok-react"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]') + .pause(2000) .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]') + .pause(2000) .click('*[data-id="workspacesSelect"] option[value="workspace_name"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]') }, @@ -59,10 +68,15 @@ module.exports = { .waitForElementVisible('*[data-id="modalDialogCustomPromptTextRename"]') // eslint-disable-next-line dot-notation .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextRename"]')['value'] = 'workspace_name_renamed' }) - .click('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') + .pause(2000) + .waitForElementPresent('span[data-id="workspacesModalDialog-modal-footer-ok-react"]') + .click('span[data-id="workspacesModalDialog-modal-footer-ok-react"]') + .pause(2000) .click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') + .pause(2000) .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]') .click('*[data-id="workspacesSelect"] option[value="workspace_name_renamed"]') + .pause(2000) .waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]') }, @@ -70,8 +84,10 @@ module.exports = { browser .click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') .click('*[data-id="workspaceDelete"]') // delete workspace_name_1 - .waitForElementVisible('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') - .click('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') + .waitForElementVisible('[data-id="workspacesModalDialogModalDialogModalFooter-react"] > span') + .pause(2000) + .click('[data-id="workspacesModalDialogModalDialogModalFooter-react"] > span') + .pause(2000) .waitForElementNotPresent('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') .end() }, diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index 42c151dd91..03c7965fd6 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -287,7 +287,7 @@ Please make a backup of your contracts and start using http://remix.ethereum.org registry.put({ api: offsetToLineColumnConverter, name: 'offsettolinecolumnconverter' }) // -------------------Terminal---------------------------------------- - + makeUdapp(blockchain, compilersArtefacts, (domEl) => terminal.logHtml(domEl)) const terminal = new Terminal( { appManager, blockchain }, { @@ -299,9 +299,9 @@ Please make a backup of your contracts and start using http://remix.ethereum.org newpos = (newpos < height - limitDown) ? newpos : height - limitDown return height - newpos } - } + }, + registry ) - makeUdapp(blockchain, compilersArtefacts, (domEl) => terminal.logHtml(domEl)) const contextualListener = new ContextualListener({ editor }) diff --git a/apps/remix-ide/src/app/components/main-panel.js b/apps/remix-ide/src/app/components/main-panel.js index 62d76cf4c3..66e6a50245 100644 --- a/apps/remix-ide/src/app/components/main-panel.js +++ b/apps/remix-ide/src/app/components/main-panel.js @@ -31,7 +31,7 @@ export class MainPanel extends AbstractPanel { render () { return yo` -
+
${this.view}
` } diff --git a/apps/remix-ide/src/app/panels/terminal.js b/apps/remix-ide/src/app/panels/terminal.js index bd474b41fb..12b95d8b06 100644 --- a/apps/remix-ide/src/app/panels/terminal.js +++ b/apps/remix-ide/src/app/panels/terminal.js @@ -1,29 +1,24 @@ -/* global Node, requestAnimationFrame */ +/* global Node, requestAnimationFrame */ // eslint-disable-line +import React from 'react' // eslint-disable-line +import ReactDOM from 'react-dom' +import { RemixUiTerminal } from '@remix-ui/terminal' // eslint-disable-line import { Plugin } from '@remixproject/engine' import * as packageJson from '../../../../../package.json' -import * as remixBleach from '../../lib/remixBleach' +const vm = require('vm') +const EventManager = require('../../lib/events') -var yo = require('yo-yo') -var javascriptserialize = require('javascript-serialize') -var jsbeautify = require('js-beautify') -var type = require('component-type') -var vm = require('vm') -var EventManager = require('../../lib/events') +const CommandInterpreterAPI = require('../../lib/cmdInterpreterAPI') +const AutoCompletePopup = require('../ui/auto-complete-popup') -var CommandInterpreterAPI = require('../../lib/cmdInterpreterAPI') -var AutoCompletePopup = require('../ui/auto-complete-popup') -var TxLogger = require('../../app/ui/txLogger') +import { CompilerImports } from '@remix-project/core-plugin' // eslint-disable-line +const globalRegistry = require('../../global/registry') +const SourceHighlighter = require('../../app/editor/sourceHighlighter') +const GistHandler = require('../../lib/gist-handler') -var csjs = require('csjs-inject') - -var css = require('./styles/terminal-styles') - -var KONSOLES = [] +const KONSOLES = [] function register (api) { KONSOLES.push(api) } -var ghostbar = yo`
` - const profile = { displayName: 'Terminal', name: 'terminal', @@ -34,77 +29,72 @@ const profile = { } class Terminal extends Plugin { - constructor (opts, api) { + constructor (opts, api, registry) { super(profile) - var self = this - self.event = new EventManager() - self.blockchain = opts.blockchain - self._api = api - self._opts = opts - self.data = { + this.fileImport = new CompilerImports() + this.gistHandler = new GistHandler() + this.event = new EventManager() + this.registry = registry + this.globalRegistry = globalRegistry + this.element = document.createElement('div') + this.element.setAttribute('class', 'panel') + this.element.setAttribute('id', 'terminal-view') + this.element.setAttribute('data-id', 'terminalContainer-view') + this.eventsDecoder = this.globalRegistry.get('eventsDecoder').api + this.txListener = this.globalRegistry.get('txlistener').api + this.sourceHighlighter = new SourceHighlighter() + this._deps = { + fileManager: this.registry.get('filemanager').api, + editor: this.registry.get('editor').api, + compilersArtefacts: this.registry.get('compilersartefacts').api, + offsetToLineColumnConverter: this.registry.get('offsettolinecolumnconverter').api + } + this.commandHelp = { + 'remix.loadgist(id)': 'Load a gist in the file explorer.', + 'remix.loadurl(url)': 'Load the given url in the file explorer. The url can be of type github, swarm, ipfs or raw http', + 'remix.execute(filepath)': 'Run the script specified by file path. If filepath is empty, script currently displayed in the editor is executed.', + 'remix.exeCurrent()': 'Run the script currently displayed in the editor', + 'remix.help()': 'Display this help message' + } + this.blockchain = opts.blockchain + this.vm = vm + this._api = api + this._opts = opts + this.config = registry.get('config').api + this.version = packageJson.version + this.data = { lineLength: opts.lineLength || 80, // ???? session: [], activeFilters: { commands: {}, input: '' }, filterFns: {} } - self._view = { el: null, bar: null, input: null, term: null, journal: null, cli: null } - self._components = {} - self._components.cmdInterpreter = new CommandInterpreterAPI(this, null, self.blockchain) - self._components.autoCompletePopup = new AutoCompletePopup(self._opts) - self._components.autoCompletePopup.event.register('handleSelect', function (input) { - const textList = self._view.input.innerText.split(' ') - textList.pop() - textList.push(input) - self._view.input.innerText = textList - self._view.input.focus() - self.putCursor2End(self._view.input) + this._view = { el: null, bar: null, input: null, term: null, journal: null, cli: null } + this._components = {} + this._components.cmdInterpreter = new CommandInterpreterAPI(this, null, this.blockchain) + this._components.autoCompletePopup = new AutoCompletePopup(this._opts) + this._commands = {} + this.commands = {} + this._JOURNAL = [] + this._jobs = [] + this._INDEX = {} + this._INDEX.all = [] + this._INDEX.allMain = [] + this._INDEX.commands = {} + this._INDEX.commandsMain = {} + if (opts.shell) this._shell = opts.shell // ??? + register(this) + this.event.register('debuggingRequested', async (hash) => { + // TODO should probably be in the run module + if (!await this._opts.appManager.isActive('debugger')) await this._opts.appManager.activatePlugin('debugger') + this.call('menuicons', 'select', 'debugger') + this.call('debugger', 'debug', hash) }) - self._commands = {} - self.commands = {} - self._JOURNAL = [] - self._jobs = [] - self._INDEX = {} - self._INDEX.all = [] - self._INDEX.allMain = [] - self._INDEX.commands = {} - self._INDEX.commandsMain = {} - self.registerCommand('html', self._blocksRenderer('html'), { activate: true }) - self.registerCommand('log', self._blocksRenderer('log'), { activate: true }) - self.registerCommand('info', self._blocksRenderer('info'), { activate: true }) - self.registerCommand('warn', self._blocksRenderer('warn'), { activate: true }) - self.registerCommand('error', self._blocksRenderer('error'), { activate: true }) - self.registerCommand('script', function execute (args, scopedCommands, append) { - var script = String(args[0]) - self._shell(script, scopedCommands, function (error, output) { - if (error) scopedCommands.error(error) - else if (output) scopedCommands.log(output) - }) - }, { activate: true }) - function basicFilter (value, query) { try { return value.indexOf(query) !== -1 } catch (e) { return false } } - - self.registerFilter('log', basicFilter) - self.registerFilter('info', basicFilter) - self.registerFilter('warn', basicFilter) - self.registerFilter('error', basicFilter) - self.registerFilter('script', basicFilter) - - if (opts.shell) self._shell = opts.shell // ??? - register(self) + this.logHtmlResponse = [] + this.logResponse = [] } onActivation () { - this.on('scriptRunner', 'log', (msg) => { - this.commands.log.apply(this.commands, msg.data) - }) - this.on('scriptRunner', 'info', (msg) => { - this.commands.info.apply(this.commands, msg.data) - }) - this.on('scriptRunner', 'warn', (msg) => { - this.commands.warn.apply(this.commands, msg.data) - }) - this.on('scriptRunner', 'error', (msg) => { - this.commands.error.apply(this.commands, msg.data) - }) + this.renderComponent() } onDeactivation () { @@ -114,690 +104,43 @@ class Terminal extends Plugin { this.off('scriptRunner', 'error') } - log (message) { - var command = this.commands[message.type] - if (typeof command === 'function') { - if (typeof message.value === 'string' && message.type === 'html') { - var el = document.createElement('div') - el.innerHTML = remixBleach.sanitize(message.value, { - list: [ - 'a', - 'b', - 'p', - 'em', - 'strong', - 'div', - 'span', - 'ul', - 'li', - 'ol', - 'hr' - ] - }) - message.value = el - } - command(message.value) - }; - } - logHtml (html) { - var command = this.commands.html - if (typeof command === 'function') command(html) + this.logHtmlResponse.push(html.innerText) + this.renderComponent() + this.resetLogHtml() } - focus () { - if (this._view.input) this._view.input.focus() + resetLogHtml () { + this.logHtmlResponse = [] } - render () { - var self = this - if (self._view.el) return self._view.el - self._view.journal = yo`
` - self._view.input = yo` - { this.focus() }} onpaste=${paste} onkeydown=${change}> - ` - self._view.input.setAttribute('spellcheck', 'false') - self._view.input.setAttribute('contenteditable', 'true') - self._view.input.setAttribute('id', 'terminalCliInput') - self._view.input.setAttribute('data-id', 'terminalCliInput') - - self._view.input.innerText = '\n' - self._view.cli = yo` -
- ${'>'} - ${self._view.input} -
- ` - - self._view.icon = yo` - ` - self._view.dragbar = yo` -
` - - self._view.pendingTxCount = yo`
0
` - self._view.inputSearch = yo` - ` - self._view.bar = yo` -
- ${self._view.dragbar} -
- ${self._view.icon} -
- -
- ${self._view.pendingTxCount} -
-
- - -
-
- - ${self._view.inputSearch} -
-
-
- ` - self._view.term = yo` -
- ${self._components.autoCompletePopup.render()} -
-
- ${self._view.journal} - ${self._view.cli} -
-
- ` - self._view.el = yo` -
- ${self._view.bar} - ${self._view.term} -
- ` - setInterval(async () => { - try { - self._view.pendingTxCount.innerHTML = await self.call('udapp', 'pendingTransactionsCount') - } catch (err) {} - }, 1000) - - function listenOnNetwork (ev) { - self.event.trigger('listenOnNetWork', [ev.currentTarget.checked]) - } - function paste (event) { - const selection = window.getSelection() - if (!selection.rangeCount) return false - event.preventDefault() - event.stopPropagation() - var clipboard = (event.clipboardData || window.clipboardData) - var text = clipboard.getData('text/plain') - text = text.replace(/[^\x20-\xFF]/gi, '') // remove non-UTF-8 characters - var temp = document.createElement('div') - temp.innerHTML = text - var textnode = document.createTextNode(temp.textContent) - selection.getRangeAt(0).insertNode(textnode) - selection.empty() - self.scroll2bottom() - placeCaretAtEnd(event.currentTarget) - } - function placeCaretAtEnd (el) { - el.focus() - var range = document.createRange() - range.selectNodeContents(el) - range.collapse(false) - var sel = window.getSelection() - sel.removeAllRanges() - sel.addRange(range) - } - function throttle (fn, wait) { - var time = Date.now() - return function debounce () { - if ((time + wait - Date.now()) < 0) { - fn.apply(this, arguments) - time = Date.now() - } - } - } - var css2 = csjs` - .anchor { - position : static; - border-top : 2px dotted blue; - height : 10px; - } - .overlay { - position : absolute; - width : 100%; - display : flex; - align-items : center; - justify-content : center; - bottom : 0; - right : 15px; - min-height : 20px; - } - .text { - z-index : 2; - color : black; - font-weight : bold; - pointer-events : none; - } - .background { - z-index : 1; - opacity : 0.8; - background-color : #a6aeba; - cursor : pointer; - } - .ul { - padding-left : 20px; - padding-bottom : 5px; - } - ` - var text = yo`
` - var background = yo`
` - var placeholder = yo`
${background}${text}
` - var inserted = false - - window.addEventListener('resize', function (event) { - self.event.trigger('resize', []) - self.event.trigger('resize', []) - }) - - function focusinput (event) { - if ( - event.altKey || - event.ctrlKey || - event.metaKey || - event.shiftKey || - event.key === 'Down' || - event.key === 'ArrowDown' || - event.key === 'Up' || - event.key === 'ArrowUp' || - event.key === 'Left' || - event.key === 'ArrowLeft' || - event.key === 'Right' || - event.key === 'ArrowRight' || - event.key === 'Esc' || - event.key === 'Escape' - ) return - - refocus() - } - - function refocus () { - self._view.input.focus() - reattach({ currentTarget: self._view.term }) - delete self.scroll2bottom - self.scroll2bottom() - } - function reattach (event) { - var el = event.currentTarget - var isBottomed = el.scrollHeight - el.scrollTop - el.clientHeight < 30 - if (isBottomed) { - if (inserted) { - text.innerText = '' - background.onclick = undefined - if (placeholder.parentElement) self._view.journal.removeChild(placeholder) - } - inserted = false - delete self.scroll2bottom - } else { - if (!inserted) self._view.journal.appendChild(placeholder) - inserted = true - check() - if (!placeholder.nextElementSibling) { - placeholder.style.display = 'none' - } else { - placeholder.style = '' - } - self.scroll2bottom = function () { - var next = placeholder.nextElementSibling - if (next) { - placeholder.style = '' - check() - var messages = 1 - while ((next = next.nextElementSibling)) messages += 1 - text.innerText = `${messages} new unread log entries` - } else { - placeholder.style.display = 'none' - } - } - } - } - function check () { - var pos1 = self._view.term.offsetHeight + self._view.term.scrollTop - (self._view.el.offsetHeight * 0.15) - var pos2 = placeholder.offsetTop - if ((pos1 - pos2) > 0) { - text.style.display = 'none' - background.style.position = 'relative' - background.style.opacity = 0.3 - background.style.right = 0 - background.style.borderBox = 'content-box' - background.style.padding = '2px' - background.style.height = (self._view.journal.offsetHeight - (placeholder.offsetTop + placeholder.offsetHeight)) + 'px' - background.onclick = undefined - background.style.cursor = 'default' - background.style.pointerEvents = 'none' - } else { - background.style = '' - text.style = '' - background.onclick = function (event) { - placeholder.scrollIntoView() - check() - } - } - } - function hover (event) { event.currentTarget.classList.toggle(css.hover) } - function minimize (event) { - event.preventDefault() - event.stopPropagation() - if (event.button === 0) { - var classList = self._view.icon.classList - classList.toggle('fa-angle-double-down') - classList.toggle('fa-angle-double-up') - self.event.trigger('resize', []) - } - } - var filtertimeout = null - function filter (event) { - if (filtertimeout) { - clearTimeout(filtertimeout) - } - filtertimeout = setTimeout(() => { - self.updateJournal({ type: 'search', value: self._view.inputSearch.value }) - self.scroll2bottom() - }, 500) - } - function clear (event) { - refocus() - self._view.journal.innerHTML = '' - } - // ----------------- resizeable ui --------------- - function mousedown (event) { - event.preventDefault() - if (event.which === 1) { - moveGhostbar(event) - document.body.appendChild(ghostbar) - document.addEventListener('mousemove', moveGhostbar) - document.addEventListener('mouseup', removeGhostbar) - document.addEventListener('keydown', cancelGhostbar) - } - } - function cancelGhostbar (event) { - if (event.keyCode === 27) { - document.body.removeChild(ghostbar) - document.removeEventListener('mousemove', moveGhostbar) - document.removeEventListener('mouseup', removeGhostbar) - document.removeEventListener('keydown', cancelGhostbar) - } - } - function moveGhostbar (event) { // @NOTE HORIZONTAL ghostbar - ghostbar.style.top = self._api.getPosition(event) + 'px' - } - function removeGhostbar (event) { - if (self._view.icon.classList.contains('fa-angle-double-up')) { - self._view.icon.classList.toggle('fa-angle-double-down') - self._view.icon.classList.toggle('fa-angle-double-up') - } - document.body.removeChild(ghostbar) - document.removeEventListener('mousemove', moveGhostbar) - document.removeEventListener('mouseup', removeGhostbar) - document.removeEventListener('keydown', cancelGhostbar) - self.event.trigger('resize', [self._api.getPosition(event)]) - } - - self._cmdHistory = [] - self._cmdIndex = -1 - self._cmdTemp = '' - - var intro = yo` -
- Welcome to Remix ${packageJson.version} -

-
You can use this terminal to:
- -
The following libraries are accessible:
- -
- ` - - self._shell('remix.help()', self.commands, () => {}) - self.commands.html(intro) - - self._components.txLogger = new TxLogger(this, self.blockchain) - self._components.txLogger.event.register('debuggingRequested', async (hash) => { - // TODO should probably be in the run module - if (!await self._opts.appManager.isActive('debugger')) await self._opts.appManager.activatePlugin('debugger') - this.call('menuicons', 'select', 'debugger') - this.call('debugger', 'debug', hash) - }) - - return self._view.el - - function wrapScript (script) { - const isKnownScript = ['remix.', 'git'].some(prefix => script.trim().startsWith(prefix)) - if (isKnownScript) return script - return ` - try { - const ret = ${script}; - if (ret instanceof Promise) { - ret.then((result) => { console.log(result) }).catch((error) => { console.log(error) }) - } else { - console.log(ret) - } - } catch (e) { - console.log(e.message) - } - ` - } - function change (event) { - if (self._components.autoCompletePopup.handleAutoComplete( - event, - self._view.input.innerText)) return - if (self._view.input.innerText.length === 0) self._view.input.innerText += '\n' - if (event.which === 13) { - if (event.ctrlKey) { // - self._view.input.innerText += '\n' - self.putCursor2End(self._view.input) - self.scroll2bottom() - self._components.autoCompletePopup.removeAutoComplete() - } else { // - self._cmdIndex = -1 - self._cmdTemp = '' - event.preventDefault() - var script = self._view.input.innerText.trim() - self._view.input.innerText = '\n' - if (script.length) { - self._cmdHistory.unshift(script) - self.commands.script(wrapScript(script)) - } - self._components.autoCompletePopup.removeAutoComplete() - } - } else if (event.which === 38) { // - var len = self._cmdHistory.length - if (len === 0) return event.preventDefault() - if (self._cmdHistory.length - 1 > self._cmdIndex) { - self._cmdIndex++ - } - self._view.input.innerText = self._cmdHistory[self._cmdIndex] - self.putCursor2End(self._view.input) - self.scroll2bottom() - } else if (event.which === 40) { // - if (self._cmdIndex > -1) { - self._cmdIndex-- - } - self._view.input.innerText = self._cmdIndex >= 0 ? self._cmdHistory[self._cmdIndex] : self._cmdTemp - self.putCursor2End(self._view.input) - self.scroll2bottom() - } else { - self._cmdTemp = self._view.input.innerText - } - } + log (message) { + this.logResponse.push(message) + this.renderComponent() + this.resetLog() } - putCursor2End (editable) { - var range = document.createRange() - range.selectNode(editable) - var child = editable - var chars - - while (child) { - if (child.lastChild) child = child.lastChild - else break - if (child.nodeType === Node.TEXT_NODE) { - chars = child.textContent.length - } else { - chars = child.innerHTML.length - } - } - - range.setEnd(child, chars) - var toStart = true - var toEnd = !toStart - range.collapse(toEnd) - - var sel = window.getSelection() - sel.removeAllRanges() - sel.addRange(range) - - editable.focus() + resetLog () { + this.logResponse = [] } - updateJournal (filterEvent) { - var self = this - var commands = self.data.activeFilters.commands - var value = filterEvent.value - if (filterEvent.type === 'select') { - commands[value] = true - if (!self._INDEX.commandsMain[value]) return - self._INDEX.commandsMain[value].forEach(item => { - item.root.steps.forEach(item => { self._JOURNAL[item.gidx] = item }) - self._JOURNAL[item.gidx] = item - }) - } else if (filterEvent.type === 'deselect') { - commands[value] = false - if (!self._INDEX.commandsMain[value]) return - self._INDEX.commandsMain[value].forEach(item => { - item.root.steps.forEach(item => { self._JOURNAL[item.gidx].hide = true }) - self._JOURNAL[item.gidx].hide = true - }) - } else if (filterEvent.type === 'search') { - if (value !== self.data.activeFilters.input) { - var query = self.data.activeFilters.input = value - var items = self._JOURNAL - for (var gidx = 0, len = items.length; gidx < len; gidx++) { - var item = items[gidx] - if (item && self.data.filterFns[item.cmd]) { - var show = query.length ? self.data.filterFns[item.cmd](item.args, query) : true - item.hide = !show - } - } - } - } - var df = document.createDocumentFragment() - self._JOURNAL.forEach(item => { - if (item && item.el && !item.hide) df.appendChild(item.el) - }) - self._view.journal.innerHTML = '' - requestAnimationFrame(function updateDOM () { - self._view.journal.appendChild(df) - }) + render () { + return this.element } - _appendItem (item) { - var self = this - var { el, gidx } = item - self._JOURNAL[gidx] = item - if (!self._jobs.length) { - requestAnimationFrame(function updateTerminal () { - self._jobs.forEach(el => self._view.journal.appendChild(el)) - self.scroll2bottom() - self._jobs = [] - }) - } - if (self.data.activeFilters.commands[item.cmd]) self._jobs.push(el) + renderComponent () { + ReactDOM.render( + , + this.element + ) } scroll2bottom () { - var self = this setTimeout(function () { - self._view.term.scrollTop = self._view.term.scrollHeight }, 0) } - - _blocksRenderer (mode) { - if (mode === 'html') { - return function logger (args, scopedCommands, append) { - if (args.length) append(args[0]) - } - } - mode = { - log: 'text-info', - info: 'text-info', - warn: 'text-warning', - error: 'text-danger' - }[mode] // defaults - - if (mode) { - const filterUndefined = (el) => el !== undefined && el !== null - return function logger (args, scopedCommands, append) { - var types = args.filter(filterUndefined).map(type) - var values = javascriptserialize.apply(null, args.filter(filterUndefined)).map(function (val, idx) { - if (typeof args[idx] === 'string') { - const el = document.createElement('div') - el.innerHTML = args[idx].replace(/(\r\n|\n|\r)/gm, '
') - val = el.children.length === 0 ? el.firstChild : el - } - if (types[idx] === 'element') val = jsbeautify.html(val) - return val - }) - if (values.length) { - append(yo`${values}`) - } - } - } else { - throw new Error('mode is not supported') - } - } - - _scopeCommands (append) { - var self = this - var scopedCommands = {} - Object.keys(self.commands).forEach(function makeScopedCommand (cmd) { - var command = self._commands[cmd] - scopedCommands[cmd] = function _command () { - var args = [...arguments] - command(args, scopedCommands, el => append(cmd, args, blockify(el))) - } - }) - return scopedCommands - } - - registerFilter (commandName, filterFn) { - this.data.filterFns[commandName] = filterFn - } - - registerCommand (name, command, opts) { - var self = this - name = String(name) - if (self._commands[name]) throw new Error(`command "${name}" exists already`) - if (typeof command !== 'function') throw new Error(`invalid command: ${command}`) - self._commands[name] = command - self._INDEX.commands[name] = [] - self._INDEX.commandsMain[name] = [] - self.commands[name] = function _command () { - var args = [...arguments] - var steps = [] - var root = { steps, cmd: name } - var ITEM = { root, cmd: name } - root.gidx = self._INDEX.allMain.push(ITEM) - 1 - root.idx = self._INDEX.commandsMain[name].push(ITEM) - 1 - function append (cmd, params, el) { - var item - if (cmd) { // subcommand - item = { el, cmd, root } - } else { // command - item = ITEM - item.el = el - cmd = name - } - item.gidx = self._INDEX.all.push(item) - 1 - item.idx = self._INDEX.commands[cmd].push(item) - 1 - item.step = steps.push(item) - 1 - item.args = params - self._appendItem(item) - } - var scopedCommands = self._scopeCommands(append) - command(args, scopedCommands, el => append(null, args, blockify(el))) - } - var help = typeof command.help === 'string' ? command.help : [ - '// no help available for:', - `terminal.commands.${name}(...)` - ].join('\n') - self.commands[name].toString = _ => { return help } - self.commands[name].help = help - self.data.activeFilters.commands[name] = opts && opts.activate - if (opts.filterFn) { - self.registerFilter(name, opts.filterFn) - } - return self.commands[name] - } - - async _shell (script, scopedCommands, done) { // default shell - if (script.indexOf('remix:') === 0) { - return done(null, 'This type of command has been deprecated and is not functionning anymore. Please run remix.help() to list available commands.') - } - var self = this - if (script.indexOf('remix.') === 0) { - // we keep the old feature. This will basically only be called when the command is querying the "remix" object. - // for all the other case, we use the Code Executor plugin - var context = domTerminalFeatures(self, scopedCommands, self.blockchain) - try { - var cmds = vm.createContext(context) - var result = vm.runInContext(script, cmds) - return done(null, result) - } catch (error) { - return done(error.message) - } - } - try { - let result - if (script.trim().startsWith('git')) { - // result = await this.call('git', 'execute', script) - } else { - result = await this.call('scriptRunner', 'execute', script) - } - if (result) self.commands.html(yo`
${result}
`) - done() - } catch (error) { - done(error.message || error) - } - } } -function domTerminalFeatures (self, scopedCommands, blockchain) { - return { - remix: self._components.cmdInterpreter - } -} - -function blockify (el) { return yo`
${el}
` } - module.exports = Terminal diff --git a/apps/remix-ide/src/app/tabs/runTab/contractDropdown.js b/apps/remix-ide/src/app/tabs/runTab/contractDropdown.js index e66ad8a5b8..a111147672 100644 --- a/apps/remix-ide/src/app/tabs/runTab/contractDropdown.js +++ b/apps/remix-ide/src/app/tabs/runTab/contractDropdown.js @@ -218,11 +218,12 @@ class ContractDropdownUI { if (this.selectContractNames.value === '') this.enableAtAddress(false) } else { this.loadType = 'other' - this.createPanel.style.display = 'none' - this.orLabel.style.display = 'none' - this.compFails.style.display = 'none' - this.contractNamesContainer.style.display = 'none' + this.createPanel.style.display = 'block' + this.orLabel.style.display = 'block' + this.contractNamesContainer.style.display = 'block' + this.selectContractNames.style.display = 'block' this.abiLabel.style.display = 'none' + if (this.selectContractNames.value === '') this.enableAtAddress(false) } } diff --git a/apps/remix-ide/src/app/tabs/test-tab.js b/apps/remix-ide/src/app/tabs/test-tab.js index 277abab11d..a41667bf3a 100644 --- a/apps/remix-ide/src/app/tabs/test-tab.js +++ b/apps/remix-ide/src/app/tabs/test-tab.js @@ -247,6 +247,14 @@ module.exports = class TestTab extends ViewPlugin { testCallback (result, runningTests) { this.testsOutput.hidden = false + let debugBtn = yo`` + if ((result.type === 'testPass' || result.type === 'testFailure') && result.debugTxHash) { + const { web3, debugTxHash } = result + debugBtn = yo`
this.startDebug(debugTxHash, web3)}> + +
` + debugBtn.style.cursor = 'pointer' + } if (result.type === 'contract') { this.testSuite = result.value if (this.testSuites) { @@ -268,29 +276,18 @@ module.exports = class TestTab extends ViewPlugin {
this.discardHighlight()} > - ✓ ${result.value} +
+ ✓ ${result.value} + ${debugBtn} +
`) } else if (result.type === 'testFailure') { if (result.hhLogs && result.hhLogs.length) this.printHHLogs(result.hhLogs, result.value) if (!result.assertMethod) { - let debugBtn = yo`` - if (result.errMsg.includes('Transaction has been reverted by the EVM')) { - const txHash = JSON.parse(result.errMsg.replace('Transaction has been reverted by the EVM:', '')).transactionHash - const { web3 } = result - debugBtn = yo`
this.startDebug(txHash, web3)} - > - -
` - debugBtn.style.visibility = 'visible' - debugBtn.style.cursor = 'pointer' - } else debugBtn.style.visibility = 'hidden' this.testsOutput.appendChild(yo`
this.highlightLocation(result.location, runningTests, result.filename)} > - ✘ ${result.value} +
+ ✘ ${result.value} + ${debugBtn} +
Error Message: "${result.errMsg}" Assertion: @@ -499,7 +499,7 @@ module.exports = class TestTab extends ViewPlugin { usingWorker: canUseWorker(currentVersion), runs } - this.testRunner.runTestSources(runningTest, compilerConfig, () => {}, () => {}, (error, result) => { + this.testRunner.runTestSources(runningTest, compilerConfig, () => {}, () => {}, null, (error, result) => { if (error) return reject(error) resolve(result) }, (url, cb) => { @@ -527,17 +527,22 @@ module.exports = class TestTab extends ViewPlugin { usingWorker: canUseWorker(currentVersion), runs } + const deployCb = async (file, contractAddress) => { + const compilerData = await this.call('compilerArtefacts', 'getCompilerAbstract', file) + await this.call('compilerArtefacts', 'addResolvedContract', contractAddress, compilerData) + } this.testRunner.runTestSources( runningTests, compilerConfig, (result) => this.testCallback(result, runningTests), (_err, result, cb) => this.resultsCallback(_err, result, cb), + deployCb, (error, result) => { this.updateFinalResult(error, result, testFilePath) callback(error) }, (url, cb) => { return this.contentImport.resolveAndSave(url).then((result) => cb(null, result)).catch((error) => cb(error.message)) - } + }, { testFilePath } ) }).catch((error) => { if (error) return // eslint-disable-line diff --git a/apps/remix-ide/src/lib/cmdInterpreterAPI.js b/apps/remix-ide/src/lib/cmdInterpreterAPI.js index 9c79cae25b..1bd834054b 100644 --- a/apps/remix-ide/src/lib/cmdInterpreterAPI.js +++ b/apps/remix-ide/src/lib/cmdInterpreterAPI.js @@ -93,7 +93,6 @@ class CmdInterpreterAPI { if (cb) cb() return } - self._components.terminal.commands.script(content) } diff --git a/apps/solidity-compiler/src/app/app.tsx b/apps/solidity-compiler/src/app/app.tsx index 00ce99f3d7..85e67924b8 100644 --- a/apps/solidity-compiler/src/app/app.tsx +++ b/apps/solidity-compiler/src/app/app.tsx @@ -6,7 +6,7 @@ import { CompilerClientApi } from './compiler' const remix = new CompilerClientApi() -export const App = () => { +export const App = () => { return (
diff --git a/apps/solidity-compiler/src/app/compiler.ts b/apps/solidity-compiler/src/app/compiler.ts index 5a6e8eafaa..4f90fa4bd3 100644 --- a/apps/solidity-compiler/src/app/compiler.ts +++ b/apps/solidity-compiler/src/app/compiler.ts @@ -30,7 +30,6 @@ const defaultCompilerParameters = { evmVersion: null, // compiler default language: 'Solidity' } - export class CompilerClientApi extends CompilerApiMixin(PluginClient) implements ICompilerApi { constructor () { super() diff --git a/libs/remix-analyzer/package.json b/libs/remix-analyzer/package.json index c5cd096a67..e9d8c22e53 100644 --- a/libs/remix-analyzer/package.json +++ b/libs/remix-analyzer/package.json @@ -19,9 +19,9 @@ } ], "dependencies": { - "@ethereumjs/block": "^3.4.0", - "@ethereumjs/tx": "^3.3.0", - "@ethereumjs/vm": "^5.5.0", + "@ethereumjs/block": "^3.5.1", + "@ethereumjs/tx": "^3.3.2", + "@ethereumjs/vm": "^5.5.3", "@remix-project/remix-astwalker": "^0.0.37", "@remix-project/remix-lib": "^0.5.7", "async": "^2.6.2", diff --git a/libs/remix-astwalker/package.json b/libs/remix-astwalker/package.json index fe3baecb9d..789a23dea9 100644 --- a/libs/remix-astwalker/package.json +++ b/libs/remix-astwalker/package.json @@ -34,9 +34,9 @@ ] }, "dependencies": { - "@ethereumjs/block": "^3.4.0", - "@ethereumjs/tx": "^3.3.0", - "@ethereumjs/vm": "^5.5.0", + "@ethereumjs/block": "^3.5.1", + "@ethereumjs/tx": "^3.3.2", + "@ethereumjs/vm": "^5.5.3", "@remix-project/remix-lib": "^0.5.7", "@types/tape": "^4.2.33", "async": "^2.6.2", diff --git a/libs/remix-core-plugin/src/lib/compiler-artefacts.ts b/libs/remix-core-plugin/src/lib/compiler-artefacts.ts index da22ffb39e..52e1c7730b 100644 --- a/libs/remix-core-plugin/src/lib/compiler-artefacts.ts +++ b/libs/remix-core-plugin/src/lib/compiler-artefacts.ts @@ -4,7 +4,7 @@ import { CompilerAbstract } from '@remix-project/remix-solidity' const profile = { name: 'compilerArtefacts', - methods: ['get', 'addResolvedContract'], + methods: ['get', 'addResolvedContract', 'getCompilerAbstract'], events: [], version: '0.0.1' } diff --git a/libs/remix-debug/package.json b/libs/remix-debug/package.json index 5be0192f86..bbc9b1042d 100644 --- a/libs/remix-debug/package.json +++ b/libs/remix-debug/package.json @@ -18,10 +18,10 @@ ], "main": "src/index.js", "dependencies": { - "@ethereumjs/block": "^3.4.0", - "@ethereumjs/common": "^2.2.0", - "@ethereumjs/tx": "^3.3.0", - "@ethereumjs/vm": "^5.5.0", + "@ethereumjs/block": "^3.5.1", + "@ethereumjs/common": "^2.5.0", + "@ethereumjs/tx": "^3.3.2", + "@ethereumjs/vm": "^5.5.3", "@remix-project/remix-astwalker": "^0.0.37", "@remix-project/remix-lib": "^0.5.7", "@remix-project/remix-simulator": "^0.2.7", diff --git a/libs/remix-lib/package.json b/libs/remix-lib/package.json index 5b25ecd191..f8cfe6ca60 100644 --- a/libs/remix-lib/package.json +++ b/libs/remix-lib/package.json @@ -14,9 +14,9 @@ ], "main": "src/index.js", "dependencies": { - "@ethereumjs/block": "^3.4.0", - "@ethereumjs/tx": "^3.3.0", - "@ethereumjs/vm": "^5.5.0", + "@ethereumjs/block": "^3.5.1", + "@ethereumjs/tx": "^3.3.2", + "@ethereumjs/vm": "^5.5.3", "async": "^2.1.2", "ethereumjs-util": "^7.0.10", "ethers": "^4.0.40", diff --git a/libs/remix-simulator/package.json b/libs/remix-simulator/package.json index 891cc3bad6..ad508bcc3a 100644 --- a/libs/remix-simulator/package.json +++ b/libs/remix-simulator/package.json @@ -14,10 +14,10 @@ ], "main": "src/index.js", "dependencies": { - "@ethereumjs/block": "^3.4.0", - "@ethereumjs/common": "^2.2.0", - "@ethereumjs/tx": "^3.3.0", - "@ethereumjs/vm": "^5.5.0", + "@ethereumjs/block": "^3.5.1", + "@ethereumjs/common": "^2.5.0", + "@ethereumjs/tx": "^3.3.2", + "@ethereumjs/vm": "^5.5.3", "@remix-project/remix-lib": "^0.5.7", "ansi-gray": "^0.1.1", "async": "^3.1.0", diff --git a/libs/remix-solidity/package.json b/libs/remix-solidity/package.json index 50ee1074a5..9fed725a41 100644 --- a/libs/remix-solidity/package.json +++ b/libs/remix-solidity/package.json @@ -15,9 +15,9 @@ } ], "dependencies": { - "@ethereumjs/block": "^3.4.0", - "@ethereumjs/tx": "^3.3.0", - "@ethereumjs/vm": "^5.5.0", + "@ethereumjs/block": "^3.5.1", + "@ethereumjs/tx": "^3.3.2", + "@ethereumjs/vm": "^5.5.3", "@remix-project/remix-lib": "^0.5.7", "async": "^2.6.2", "eslint-scope": "^5.0.0", diff --git a/libs/remix-tests/package.json b/libs/remix-tests/package.json index 990ad5a6a1..914bfebcc2 100644 --- a/libs/remix-tests/package.json +++ b/libs/remix-tests/package.json @@ -35,10 +35,10 @@ }, "homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-tests#readme", "dependencies": { - "@ethereumjs/block": "^3.4.0", - "@ethereumjs/common": "^2.2.0", - "@ethereumjs/tx": "^3.3.0", - "@ethereumjs/vm": "^5.5.0", + "@ethereumjs/block": "^3.5.1", + "@ethereumjs/common": "^2.5.0", + "@ethereumjs/tx": "^3.3.2", + "@ethereumjs/vm": "^5.5.3", "@remix-project/remix-lib": "^0.5.7", "@remix-project/remix-simulator": "^0.2.7", "@remix-project/remix-solidity": "^0.4.7", diff --git a/libs/remix-tests/src/compiler.ts b/libs/remix-tests/src/compiler.ts index e77fdcdcfc..e15bb656e4 100644 --- a/libs/remix-tests/src/compiler.ts +++ b/libs/remix-tests/src/compiler.ts @@ -171,8 +171,9 @@ export function compileFileOrFiles (filename: string, isDirectory: boolean, opts * @param cb Callback */ export function compileContractSources (sources: SrcIfc, compilerConfig: CompilerConfiguration, importFileCb: any, opts: any, cb): void { - let compiler, filepath: string + let compiler const accounts: string[] = opts.accounts || [] + const filepath = opts.testFilePath || '' // Iterate over sources keys. Inject test libraries. Inject test library import statements. if (!('remix_tests.sol' in sources) && !('tests.sol' in sources)) { sources['tests.sol'] = { content: require('../sol/tests.sol.js') } diff --git a/libs/remix-tests/src/deployer.ts b/libs/remix-tests/src/deployer.ts index aeefac3faa..d700b08de5 100644 --- a/libs/remix-tests/src/deployer.ts +++ b/libs/remix-tests/src/deployer.ts @@ -11,7 +11,7 @@ import { compilationInterface } from './types' * @param callback Callback */ -export function deployAll (compileResult: compilationInterface, web3: Web3, withDoubleGas: boolean, callback) { +export function deployAll (compileResult: compilationInterface, web3: Web3, withDoubleGas: boolean, deployCb, callback) { const compiledObject = {} const contracts = {} let accounts: string[] = [] @@ -70,7 +70,7 @@ export function deployAll (compileResult: compilationInterface, web3: Web3, with deployObject.send({ from: accounts[0], gas: gas - }).on('receipt', function (receipt) { + }).on('receipt', async function (receipt) { contractObject.options.address = receipt.contractAddress contractObject.options.from = accounts[0] contractObject.options.gas = 5000 * 1000 @@ -79,6 +79,7 @@ export function deployAll (compileResult: compilationInterface, web3: Web3, with contracts[contractName] = contractObject contracts[contractName].filename = filename + if (deployCb) await deployCb(filename, receipt.contractAddress) callback(null, { receipt: { contractAddress: receipt.contractAddress } }) // TODO this will only work with JavaScriptV VM }).on('error', function (err) { console.error(err) diff --git a/libs/remix-tests/src/runTestFiles.ts b/libs/remix-tests/src/runTestFiles.ts index b447452f22..d9ed0c2b6e 100644 --- a/libs/remix-tests/src/runTestFiles.ts +++ b/libs/remix-tests/src/runTestFiles.ts @@ -61,13 +61,13 @@ export function runTestFiles (filepath: string, isDirectory: boolean, web3: Web3 for (const filename in asts) { if (filename.endsWith('_test.sol')) { sourceASTs[filename] = asts[filename].ast } } - deployAll(compilationResult, web3, false, (err, contracts) => { + deployAll(compilationResult, web3, false, null, (err, contracts) => { if (err) { // If contract deployment fails because of 'Out of Gas' error, try again with double gas // This is temporary, should be removed when remix-tests will have a dedicated UI to // accept deployment params from UI if (err.message.includes('The contract code couldn\'t be stored, please check your gas limit')) { - deployAll(compilationResult, web3, true, (error, contracts) => { + deployAll(compilationResult, web3, true, null, (error, contracts) => { if (error) next([{ message: 'contract deployment failed after trying twice: ' + error.message, severity: 'error' }]) // IDE expects errors in array else next(null, compilationResult, contracts) }) diff --git a/libs/remix-tests/src/runTestSources.ts b/libs/remix-tests/src/runTestSources.ts index 4894b713eb..ecc35aa3f2 100644 --- a/libs/remix-tests/src/runTestSources.ts +++ b/libs/remix-tests/src/runTestSources.ts @@ -39,7 +39,7 @@ export class UnitTestRunner { * @param importFileCb Import file callback * @param opts Options */ - async runTestSources (contractSources: SrcIfc, compilerConfig: CompilerConfiguration, testCallback, resultCallback, finalCallback: any, importFileCb, opts: Options) { + async runTestSources (contractSources: SrcIfc, compilerConfig: CompilerConfiguration, testCallback, resultCallback, deployCb:any, finalCallback: any, importFileCb, opts: Options) { opts = opts || {} const sourceASTs: any = {} const web3 = opts.web3 || await this.createWeb3Provider() @@ -53,19 +53,19 @@ export class UnitTestRunner { }) }, (next) => { - compileContractSources(contractSources, compilerConfig, importFileCb, { accounts, event: this.event }, next) + compileContractSources(contractSources, compilerConfig, importFileCb, { accounts, testFilePath: opts.testFilePath, event: this.event }, next) }, function deployAllContracts (compilationResult: compilationInterface, asts: ASTInterface, next) { for (const filename in asts) { if (filename.endsWith('_test.sol')) { sourceASTs[filename] = asts[filename].ast } } - deployAll(compilationResult, web3, false, (err, contracts) => { + deployAll(compilationResult, web3, false, deployCb, (err, contracts) => { if (err) { // If contract deployment fails because of 'Out of Gas' error, try again with double gas // This is temporary, should be removed when remix-tests will have a dedicated UI to // accept deployment params from UI if (err.message.includes('The contract code couldn\'t be stored, please check your gas limit')) { - deployAll(compilationResult, web3, true, (error, contracts) => { + deployAll(compilationResult, web3, true, deployCb, (error, contracts) => { if (error) next([{ message: 'contract deployment failed after trying twice: ' + error.message, severity: 'error' }]) // IDE expects errors in array else next(null, compilationResult, contracts) }) diff --git a/libs/remix-tests/src/testRunner.ts b/libs/remix-tests/src/testRunner.ts index 102dae0bf5..fe5014d2f9 100644 --- a/libs/remix-tests/src/testRunner.ts +++ b/libs/remix-tests/src/testRunner.ts @@ -244,6 +244,7 @@ export function runTest (testName: string, testObject: any, contractDetails: Com if (func.inputs && func.inputs.length > 0) { return resultsCallback(new Error(`Method '${func.name}' can not have parameters inside a test contract`), { passingNum, failureNum, timePassed }) } const method = testObject.methods[func.name].apply(testObject.methods[func.name], []) const startTime = Date.now() + let debugTxHash:string if (func.constant) { sendParams = {} const tagTimestamp = 'remix_tests_tag' + Date.now() @@ -253,13 +254,16 @@ export function runTest (testName: string, testObject: any, contractDetails: Com let tagTxHash if (web3.eth && web3.eth.getHashFromTagBySimulator) tagTxHash = await web3.eth.getHashFromTagBySimulator(tagTimestamp) if (web3.eth && web3.eth.getHHLogsForTx) hhLogs = await web3.eth.getHHLogsForTx(tagTxHash) + debugTxHash = tagTxHash if (result) { const resp: TestResultInterface = { type: 'testPass', value: changeCase.sentenceCase(func.name), filename: testObject.filename, time: time, - context: testName + context: testName, + web3, + debugTxHash } if (hhLogs && hhLogs.length) resp.hhLogs = hhLogs testCallback(undefined, resp) @@ -272,7 +276,9 @@ export function runTest (testName: string, testObject: any, contractDetails: Com filename: testObject.filename, time: time, errMsg: 'function returned false', - context: testName + context: testName, + web3, + debugTxHash } if (hhLogs && hhLogs.length) resp.hhLogs = hhLogs testCallback(undefined, resp) @@ -293,6 +299,7 @@ export function runTest (testName: string, testObject: any, contractDetails: Com sendParams.gas = 10000000 * 8 method.send(sendParams).on('receipt', async (receipt) => { try { + debugTxHash = receipt.transactionHash if (web3.eth && web3.eth.getHHLogsForTx) hhLogs = await web3.eth.getHHLogsForTx(receipt.transactionHash) const time: number = (Date.now() - startTime) / 1000.0 const assertionEventHashes = assertionEvents.map(e => Web3.utils.sha3(e.name + '(' + e.params.join() + ')')) @@ -322,7 +329,8 @@ export function runTest (testName: string, testObject: any, contractDetails: Com returned: testEvent[3], expected: testEvent[4], location, - web3 + web3, + debugTxHash } if (hhLogs && hhLogs.length) resp.hhLogs = hhLogs testCallback(undefined, resp) @@ -341,7 +349,9 @@ export function runTest (testName: string, testObject: any, contractDetails: Com value: changeCase.sentenceCase(func.name), filename: testObject.filename, time: time, - context: testName + context: testName, + web3, + debugTxHash } if (hhLogs && hhLogs.length) resp.hhLogs = hhLogs testCallback(undefined, resp) @@ -380,6 +390,7 @@ export function runTest (testName: string, testObject: any, contractDetails: Com const txHash = JSON.parse(err.message.replace('Transaction has been reverted by the EVM:', '')).transactionHash if (web3.eth && web3.eth.getHHLogsForTx) hhLogs = await web3.eth.getHHLogsForTx(txHash) if (hhLogs && hhLogs.length) resp.hhLogs = hhLogs + resp.debugTxHash = txHash } testCallback(undefined, resp) failureNum += 1 diff --git a/libs/remix-tests/src/types.ts b/libs/remix-tests/src/types.ts index fb52c56bae..fb03e94887 100644 --- a/libs/remix-tests/src/types.ts +++ b/libs/remix-tests/src/types.ts @@ -38,6 +38,7 @@ export interface TestResultInterface { location?: string hhLogs?: [] web3?: any + debugTxHash?: string } export interface TestCbInterface { (error: Error | null | undefined, result: TestResultInterface) : void; @@ -48,6 +49,7 @@ export interface ResultCbInterface { export interface Options { accounts?: string[] | null, + testFilePath?: string web3?: any } diff --git a/libs/remix-tests/tests/examples_0/assert_ok_without_console_test.sol b/libs/remix-tests/tests/examples_0/assert_ok_without_console_test.sol new file mode 100644 index 0000000000..030552b16a --- /dev/null +++ b/libs/remix-tests/tests/examples_0/assert_ok_without_console_test.sol @@ -0,0 +1,12 @@ +import "remix_tests.sol"; // this import is automatically injected by Remix. + +contract AssertOkTest { + + function okPassTest() public { + Assert.ok(true, "okPassTest passes"); + } + + function okFailTest() public { + Assert.ok(false, "okFailTest fails"); + } +} \ No newline at end of file diff --git a/libs/remix-tests/tests/testRunner.cli.spec_disabled.ts b/libs/remix-tests/tests/testRunner.cli.spec.ts similarity index 91% rename from libs/remix-tests/tests/testRunner.cli.spec_disabled.ts rename to libs/remix-tests/tests/testRunner.cli.spec.ts index 91174355af..496f6f4936 100644 --- a/libs/remix-tests/tests/testRunner.cli.spec_disabled.ts +++ b/libs/remix-tests/tests/testRunner.cli.spec.ts @@ -10,7 +10,9 @@ describe('testRunner: remix-tests CLI', () => { if(result) { const dirContent = result.stdout.toString() // Install dependencies if 'node_modules' is not already present - if(!dirContent.includes('node_modules')) execSync('npm install', { cwd: resolve(__dirname + '/../../../dist/libs/remix-tests') }) + if(!dirContent.includes('node_modules')) { + execSync('npm install', { cwd: resolve(__dirname + '/../../../dist/libs/remix-tests') }) + } } @@ -40,7 +42,7 @@ Commands: }) test('remix-tests running a test file', () => { - const res = spawnSync(executablePath, [resolve(__dirname + '/examples_0/assert_ok_test.sol')]) + const res = spawnSync(executablePath, [resolve(__dirname + '/examples_0/assert_ok_without_console_test.sol')]) // match initial lines expect(res.stdout.toString().trim()).toMatch(/:: Running remix-tests - Unit testing for solidity ::/) expect(res.stdout.toString().trim()).toMatch(/creation of library remix_tests.sol:Assert pending.../) @@ -55,7 +57,7 @@ Commands: }) test('remix-tests running a test file with custom compiler version', () => { - const res = spawnSync(executablePath, ['--compiler', '0.7.4', resolve(__dirname + '/examples_0/assert_ok_test.sol')]) + const res = spawnSync(executablePath, ['--compiler', '0.7.4', resolve(__dirname + '/examples_0/assert_ok_without_console_test.sol')]) // match initial lines expect(res.stdout.toString().trim().includes('Compiler version set to 0.7.4. Latest version is')).toBeTruthy() expect(res.stdout.toString().trim().includes('Loading remote solc version v0.7.4+commit.3f05b770 ...')).toBeTruthy() @@ -69,13 +71,13 @@ Commands: }) test('remix-tests running a test file with unavailable custom compiler version (should fail)', () => { - const res = spawnSync(executablePath, ['--compiler', '1.10.4', resolve(__dirname + '/examples_0/assert_ok_test.sol')]) + const res = spawnSync(executablePath, ['--compiler', '1.10.4', resolve(__dirname + '/examples_0/assert_ok_without_console_test.sol')]) // match initial lines expect(res.stdout.toString().trim().includes('No compiler found in releases with version 1.10.4')).toBeTruthy() }) test('remix-tests running a test file with custom EVM', () => { - const res = spawnSync(executablePath, ['--evm', 'petersburg', resolve(__dirname + '/examples_0/assert_ok_test.sol')]) + const res = spawnSync(executablePath, ['--evm', 'petersburg', resolve(__dirname + '/examples_0/assert_ok_without_console_test.sol')]) // match initial lines expect(res.stdout.toString().trim().includes('EVM set to petersburg')).toBeTruthy() expect(res.stdout.toString().trim()).toMatch(/:: Running remix-tests - Unit testing for solidity ::/) @@ -88,7 +90,7 @@ Commands: }) test('remix-tests running a test file by enabling optimization', () => { - const res = spawnSync(executablePath, ['--optimize', 'true', resolve(__dirname + '/examples_0/assert_ok_test.sol')]) + const res = spawnSync(executablePath, ['--optimize', 'true', resolve(__dirname + '/examples_0/assert_ok_without_console_test.sol')]) // match initial lines expect(res.stdout.toString().trim().includes('Optimization is enabled')).toBeTruthy() expect(res.stdout.toString().trim()).toMatch(/:: Running remix-tests - Unit testing for solidity ::/) @@ -101,7 +103,7 @@ Commands: }) test('remix-tests running a test file by enabling optimization and setting runs', () => { - const res = spawnSync(executablePath, ['--optimize', 'true', '--runs', '300', resolve(__dirname + '/examples_0/assert_ok_test.sol')]) + const res = spawnSync(executablePath, ['--optimize', 'true', '--runs', '300', resolve(__dirname + '/examples_0/assert_ok_without_console_test.sol')]) // match initial lines expect(res.stdout.toString().trim().includes('Optimization is enabled')).toBeTruthy() expect(res.stdout.toString().trim().includes('Runs set to 300')).toBeTruthy() @@ -115,13 +117,13 @@ Commands: }) test('remix-tests running a test file without enabling optimization and setting runs (should fail)', () => { - const res = spawnSync(executablePath, ['--runs', '300', resolve(__dirname + '/examples_0/assert_ok_test.sol')]) + const res = spawnSync(executablePath, ['--runs', '300', resolve(__dirname + '/examples_0/assert_ok_without_console_test.sol')]) // match initial lines expect(res.stdout.toString().trim().includes('Optimization should be enabled for runs')).toBeTruthy() }) test('remix-tests running a test file with all options', () => { - const res = spawnSync(executablePath, ['--compiler', '0.7.5', '--evm', 'istanbul', '--optimize', 'true', '--runs', '250', resolve(__dirname + '/examples_0/assert_ok_test.sol')]) + const res = spawnSync(executablePath, ['--compiler', '0.7.5', '--evm', 'istanbul', '--optimize', 'true', '--runs', '250', resolve(__dirname + '/examples_0/assert_ok_without_console_test.sol')]) // match initial lines expect(res.stdout.toString().trim().includes('Compiler version set to 0.7.5. Latest version is')).toBeTruthy() expect(res.stdout.toString().trim().includes('Loading remote solc version v0.7.5+commit.eb77ed08 ...')).toBeTruthy() diff --git a/libs/remix-tests/tests/testRunner.spec.ts b/libs/remix-tests/tests/testRunner.spec.ts index 351daf23f4..928491faa5 100644 --- a/libs/remix-tests/tests/testRunner.spec.ts +++ b/libs/remix-tests/tests/testRunner.spec.ts @@ -67,7 +67,7 @@ async function compileAndDeploy(filename: string, callback: Function) { } try { compilationData = compilationResult - deployAll(compilationResult, web3, false, next) + deployAll(compilationResult, web3, false, null, next) } catch (e) { throw e } @@ -128,8 +128,8 @@ describe('testRunner', () => { deepEqualExcluding(tests, [ { type: 'accountList', value: accounts }, { type: 'contract', value: 'AssertOkTest', filename: __dirname + '/examples_0/assert_ok_test.sol' }, - { type: 'testPass', value: 'Ok pass test', filename: __dirname + '/examples_0/assert_ok_test.sol', context: 'AssertOkTest', hhLogs: hhLogs1 }, - { type: 'testFailure', value: 'Ok fail test', filename: __dirname + '/examples_0/assert_ok_test.sol', errMsg: 'okFailTest fails', context: 'AssertOkTest', hhLogs: hhLogs2, assertMethod: 'ok', location: '370:36:0', expected: 'true', returned: 'false'}, + { type: 'testPass', debugTxHash: '0x5b665752a4faf83229259b9b2811d3295be0af633b0051d4b90042283ef55707', value: 'Ok pass test', filename: __dirname + '/examples_0/assert_ok_test.sol', context: 'AssertOkTest', hhLogs: hhLogs1 }, + { type: 'testFailure', debugTxHash: '0xa0a30ad042a7fc3495f72be7ba788d705888ffbbec7173f60bb27e07721510f2',value: 'Ok fail test', filename: __dirname + '/examples_0/assert_ok_test.sol', errMsg: 'okFailTest fails', context: 'AssertOkTest', hhLogs: hhLogs2, assertMethod: 'ok', location: '370:36:0', expected: 'true', returned: 'false'}, ], ['time', 'web3']) }) @@ -158,18 +158,18 @@ describe('testRunner', () => { deepEqualExcluding(tests, [ { type: 'accountList', value: accounts }, { type: 'contract', value: 'AssertEqualTest', filename: __dirname + '/examples_0/assert_equal_test.sol' }, - { type: 'testPass', value: 'Equal uint pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, - { type: 'testFailure', value: 'Equal uint fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalUintFailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '273:57:0', expected: '2', returned: '1'}, - { type: 'testPass', value: 'Equal int pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, - { type: 'testFailure', value: 'Equal int fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalIntFailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '493:45:0', expected: '2', returned: '-1'}, - { type: 'testPass', value: 'Equal bool pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, - { type: 'testFailure', value: 'Equal bool fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalBoolFailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '708:52:0', expected: false, returned: true}, - { type: 'testPass', value: 'Equal address pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, - { type: 'testFailure', value: 'Equal address fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalAddressFailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '1015:130:0', expected: '0x1c6637567229159d1eFD45f95A6675e77727E013', returned: '0x7994f14563F39875a2F934Ce42cAbF48a93FdDA9'}, - { type: 'testPass', value: 'Equal bytes32 pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, - { type: 'testFailure', value: 'Equal bytes32 fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalBytes32FailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '1670:48:0', expected: '0x72656d6978000000000000000000000000000000000000000000000000000000', returned: '0x72656d6979000000000000000000000000000000000000000000000000000000'}, - { type: 'testPass', value: 'Equal string pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, - { type: 'testFailure', value: 'Equal string fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalStringFailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '1916:81:0', expected: 'remix-tests', returned: 'remix'} + { type: 'testPass', debugTxHash: '0xbe77baee10f8a044a68fbb83abf57ce1ca63b8739f3b2dcd122dab0ee44fd0e9', value: 'Equal uint pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, + { type: 'testFailure', debugTxHash: '0xfc9691b0cd0a49dbefed5cd3fad32158dd229e5bb7b0eb11da3c72054eafae8b', value: 'Equal uint fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalUintFailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '273:57:0', expected: '2', returned: '1'}, + { type: 'testPass', debugTxHash: '0xbc142789e5a51841781536a9291c9022896b0c7453140fdc98996638c0d76045', value: 'Equal int pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, + { type: 'testFailure', debugTxHash: '0xcc28211a4ab3149b1122fb47f45529a4edddbafa076d2338cc3754ab0629eaa1', value: 'Equal int fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalIntFailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '493:45:0', expected: '2', returned: '-1'}, + { type: 'testPass', debugTxHash: '0x4f5570fc7da86f09aafb436ff3b4c46aa885f71680a233234433d0ef0346206b', value: 'Equal bool pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, + { type: 'testFailure', debugTxHash: '0x28dc2d146dee77a2d6446efb088e5f9d008a3c7a14116e798401b68470da017f', value: 'Equal bool fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalBoolFailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '708:52:0', expected: false, returned: true}, + { type: 'testPass', debugTxHash: '0x0abc8fa8831efa3a8c82c758d045c1382f71b6a7f7e9135ffbe9e40059f84617', value: 'Equal address pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, + { type: 'testFailure', debugTxHash: '0x5ec200fb053539b8165a6b6ab36e9229a870c4752b0d6ff2786c4d5a66d5b35d', value: 'Equal address fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalAddressFailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '1015:130:0', expected: '0x1c6637567229159d1eFD45f95A6675e77727E013', returned: '0x7994f14563F39875a2F934Ce42cAbF48a93FdDA9'}, + { type: 'testPass', debugTxHash: '0xb6c34f5baa6916569d122bcb1210fcd07fb126f4b859fea58db5328e5f1dab85', value: 'Equal bytes32 pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, + { type: 'testFailure', debugTxHash: '0xb3af74a384b8b6ddacbc03a480ae48e233415b1570717ca7023530023a871be6', value: 'Equal bytes32 fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalBytes32FailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '1670:48:0', expected: '0x72656d6978000000000000000000000000000000000000000000000000000000', returned: '0x72656d6979000000000000000000000000000000000000000000000000000000'}, + { type: 'testPass', debugTxHash: '0x8537e74941b511b5c745b398e55435748adcdf637659247e0d573fb681ee4833', value: 'Equal string pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, + { type: 'testFailure', debugTxHash: '0x30d44498e63ac51f1412062b849144c103e19a4dc9daf81c5e84bd984ef738a6', value: 'Equal string fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalStringFailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '1916:81:0', expected: 'remix-tests', returned: 'remix'} ], ['time', 'web3']) }) }) @@ -197,18 +197,18 @@ describe('testRunner', () => { deepEqualExcluding(tests, [ { type: 'accountList', value: accounts }, { type: 'contract', value: 'AssertNotEqualTest', filename: __dirname + '/examples_0/assert_notEqual_test.sol' }, - { type: 'testPass', value: 'Not equal uint pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, - { type: 'testFailure', value: 'Not equal uint fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualUintFailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '288:63:0', expected: '1', returned: '1'}, - { type: 'testPass', value: 'Not equal int pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, - { type: 'testFailure', value: 'Not equal int fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualIntFailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '525:52:0', expected: '-2', returned: '-2'}, - { type: 'testPass', value: 'Not equal bool pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, - { type: 'testFailure', value: 'Not equal bool fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualBoolFailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '760:57:0', expected: true, returned: true}, - { type: 'testPass', value: 'Not equal address pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, - { type: 'testFailure', value: 'Not equal address fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualAddressFailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '1084:136:0', expected: 0x7994f14563F39875a2F934Ce42cAbF48a93FdDA9, returned: 0x7994f14563F39875a2F934Ce42cAbF48a93FdDA9}, - { type: 'testPass', value: 'Not equal bytes32 pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, - { type: 'testFailure', value: 'Not equal bytes32 fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualBytes32FailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '1756:54:0', expected: '0x72656d6978000000000000000000000000000000000000000000000000000000', returned: '0x72656d6978000000000000000000000000000000000000000000000000000000'}, - { type: 'testPass', value: 'Not equal string pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, - { type: 'testFailure', value: 'Not equal string fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualStringFailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '2026:81:0', expected: 'remix', returned: 'remix'}, + { type: 'testPass', debugTxHash: '0xb0ac5cde13a5005dc1b4efbb66fb3a5d6f0697467aedd6165ed1c8ee552939bc', value: 'Not equal uint pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, + { type: 'testFailure', debugTxHash: '0x69d25ed9b9a010e97e0f7282313d4018c77a8873fd5f2e0b12483701febdd6b4', value: 'Not equal uint fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualUintFailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '288:63:0', expected: '1', returned: '1'}, + { type: 'testPass', debugTxHash: '0x19a743cfb44b273c78b3271d603412d31087974309a70595bc5d15097a52c5c5', value: 'Not equal int pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, + { type: 'testFailure', debugTxHash: '0x8dfce61b71a9ea65fb00c471370413779626f290b71d41f8be8408674e64f4d2', value: 'Not equal int fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualIntFailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '525:52:0', expected: '-2', returned: '-2'}, + { type: 'testPass', debugTxHash: '0x12bc9eb3a653ebe4c7ab954c144dab4848295c88d71d17cb86a41e8a004419ba', value: 'Not equal bool pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, + { type: 'testFailure', debugTxHash: '0x901b9cd631f8f29841243a257d1914060b9c36d88ee7d8b624de76dad4a34c85', value: 'Not equal bool fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualBoolFailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '760:57:0', expected: true, returned: true}, + { type: 'testPass', debugTxHash: '0xf9e48bac26d3a2871ceb92974b897cae61e60bbc4db115b7e8eff4ac0390e4a5', value: 'Not equal address pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, + { type: 'testFailure', debugTxHash: '0x4e83a17426bc79b147ddd30a5434a2430a8302b3ce78b6979088ac5eceac5d3c', value: 'Not equal address fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualAddressFailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '1084:136:0', expected: 0x7994f14563F39875a2F934Ce42cAbF48a93FdDA9, returned: 0x7994f14563F39875a2F934Ce42cAbF48a93FdDA9}, + { type: 'testPass', debugTxHash: '0xfb4b30bb8373eeb6b09e38ad07880b86654f72780b41d7cf7554a112a04a0f53', value: 'Not equal bytes32 pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, + { type: 'testFailure', debugTxHash: '0x43edf8bc68229415ee63f63f5303351a0dfecf9f3eb2ec35ffd2c10c60cf7005', value: 'Not equal bytes32 fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualBytes32FailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '1756:54:0', expected: '0x72656d6978000000000000000000000000000000000000000000000000000000', returned: '0x72656d6978000000000000000000000000000000000000000000000000000000'}, + { type: 'testPass', debugTxHash: '0x712932edc040596e2b02ddc06a48b773f5fccc7346d55cefc5d1c52528ce3168', value: 'Not equal string pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, + { type: 'testFailure', debugTxHash: '0x961ce7425fdd4049aeb678ea20a0441eb837c1fe26b0d010593fc07d668321e6', value: 'Not equal string fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualStringFailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '2026:81:0', expected: 'remix', returned: 'remix'}, ], ['time', 'web3']) }) }) @@ -235,14 +235,14 @@ describe('testRunner', () => { deepEqualExcluding(tests, [ { type: 'accountList', value: accounts }, { type: 'contract', value: 'AssertGreaterThanTest', filename: __dirname + '/examples_0/assert_greaterThan_test.sol' }, - { type: 'testPass', value: 'Greater than uint pass test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', context: 'AssertGreaterThanTest' }, - { type: 'testFailure', value: 'Greater than uint fail test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', errMsg: 'greaterThanUintFailTest fails', context: 'AssertGreaterThanTest', assertMethod: 'greaterThan', location: '303:69:0', expected: '4', returned: '1'}, - { type: 'testPass', value: 'Greater than int pass test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', context: 'AssertGreaterThanTest' }, - { type: 'testFailure', value: 'Greater than int fail test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', errMsg: 'greaterThanIntFailTest fails', context: 'AssertGreaterThanTest', assertMethod: 'greaterThan', location: '569:67:0', expected: '1', returned: '-1'}, - { type: 'testPass', value: 'Greater than uint int pass test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', context: 'AssertGreaterThanTest' }, - { type: 'testFailure', value: 'Greater than uint int fail test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', errMsg: 'greaterThanUintIntFailTest fails', context: 'AssertGreaterThanTest', assertMethod: 'greaterThan', location: '845:71:0', expected: '2', returned: '1'}, - { type: 'testPass', value: 'Greater than int uint pass test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', context: 'AssertGreaterThanTest' }, - { type: 'testFailure', value: 'Greater than int uint fail test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', errMsg: 'greaterThanIntUintFailTest fails', context: 'AssertGreaterThanTest', assertMethod: 'greaterThan', location: '1125:76:0', expected: '115792089237316195423570985008687907853269984665640564039457584007913129639836', returned: '100'} + { type: 'testPass', debugTxHash: '0x81cf46560b522280ac60bd5c8efedad4598957d33435a898c23eefb13ca6104f', value: 'Greater than uint pass test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', context: 'AssertGreaterThanTest' }, + { type: 'testFailure', debugTxHash: '0x7090dc27ac06e1afd66963992bdd9188200d0404d43b95cfa5d925bbe6eba3ed', value: 'Greater than uint fail test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', errMsg: 'greaterThanUintFailTest fails', context: 'AssertGreaterThanTest', assertMethod: 'greaterThan', location: '303:69:0', expected: '4', returned: '1'}, + { type: 'testPass', debugTxHash: '0xd57b40ed464763baf128f8a72efcd198e825e0b8f498cef90aed23045d0196db', value: 'Greater than int pass test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', context: 'AssertGreaterThanTest' }, + { type: 'testFailure', debugTxHash: '0x6c7b84bd8fc452b7839e11129d3818fa69dfd9b914e55556b39fdc68b70fc1b5', value: 'Greater than int fail test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', errMsg: 'greaterThanIntFailTest fails', context: 'AssertGreaterThanTest', assertMethod: 'greaterThan', location: '569:67:0', expected: '1', returned: '-1'}, + { type: 'testPass', debugTxHash: '0xc5883db70b83a1d3afff24a9f0555de3edd7776e5ec157cc2110e426e5be2594', value: 'Greater than uint int pass test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', context: 'AssertGreaterThanTest' }, + { type: 'testFailure', debugTxHash: '0xa534085a6bbdcf73a68bdef6a1421218c11ac0ec1af398f9445defeea31cea6e', value: 'Greater than uint int fail test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', errMsg: 'greaterThanUintIntFailTest fails', context: 'AssertGreaterThanTest', assertMethod: 'greaterThan', location: '845:71:0', expected: '2', returned: '1'}, + { type: 'testPass', debugTxHash: '0x36a7139445d76f6072fab4cc0717461068276748622c0dfc3f092d548b197de8', value: 'Greater than int uint pass test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', context: 'AssertGreaterThanTest' }, + { type: 'testFailure', debugTxHash: '0x7890f7b8f2eabae581b6f70d55d2d3bfa64ddd7753d5f892dbdf86ced96945fe', value: 'Greater than int uint fail test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', errMsg: 'greaterThanIntUintFailTest fails', context: 'AssertGreaterThanTest', assertMethod: 'greaterThan', location: '1125:76:0', expected: '115792089237316195423570985008687907853269984665640564039457584007913129639836', returned: '100'} ], ['time', 'web3']) }) }) @@ -270,14 +270,14 @@ describe('testRunner', () => { deepEqualExcluding(tests, [ { type: 'accountList', value: accounts }, { type: 'contract', value: 'AssertLesserThanTest', filename: __dirname + '/examples_0/assert_lesserThan_test.sol' }, - { type: 'testPass', value: 'Lesser than uint pass test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', context: 'AssertLesserThanTest' }, - { type: 'testFailure', value: 'Lesser than uint fail test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', errMsg: 'lesserThanUintFailTest fails', context: 'AssertLesserThanTest', assertMethod: 'lesserThan', location: '298:67:0', expected: '2', returned: '4'}, - { type: 'testPass', value: 'Lesser than int pass test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', context: 'AssertLesserThanTest' }, - { type: 'testFailure', value: 'Lesser than int fail test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', errMsg: 'lesserThanIntFailTest fails', context: 'AssertLesserThanTest', assertMethod: 'lesserThan', location: '557:65:0', expected: '-1', returned: '1'}, - { type: 'testPass', value: 'Lesser than uint int pass test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', context: 'AssertLesserThanTest' }, - { type: 'testFailure', value: 'Lesser than uint int fail test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', errMsg: 'lesserThanUintIntFailTest fails', context: 'AssertLesserThanTest', assertMethod: 'lesserThan', location: '826:71:0', expected: '-1', returned: '115792089237316195423570985008687907853269984665640564039457584007913129639935'}, - { type: 'testPass', value: 'Lesser than int uint pass test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', context: 'AssertLesserThanTest' }, - { type: 'testFailure', value: 'Lesser than int uint fail test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', errMsg: 'lesserThanIntUintFailTest fails', context: 'AssertLesserThanTest', assertMethod: 'lesserThan', location: '1105:69:0', expected: '1', returned: '1'}, + { type: 'testPass', debugTxHash: '0x47875047c1fff8a7b1cc1603418960cd2a3afe8a9c59337b19fb463a85d6e47e', value: 'Lesser than uint pass test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', context: 'AssertLesserThanTest' }, + { type: 'testFailure', debugTxHash: '0xf6fd459d0b28d0d85c56dd69d953331291e1c234c8a263150252e35da0ed6671', value: 'Lesser than uint fail test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', errMsg: 'lesserThanUintFailTest fails', context: 'AssertLesserThanTest', assertMethod: 'lesserThan', location: '298:67:0', expected: '2', returned: '4'}, + { type: 'testPass', debugTxHash: '0x761d95111764af396634474899ff1db218d5e514d6de6bc3260af15b1f5b929f', value: 'Lesser than int pass test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', context: 'AssertLesserThanTest' }, + { type: 'testFailure', debugTxHash: '0xc17697ef2df92c22707639caa9355bdf0d98dbb18157e72b8b257bb0eb2beb4e', value: 'Lesser than int fail test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', errMsg: 'lesserThanIntFailTest fails', context: 'AssertLesserThanTest', assertMethod: 'lesserThan', location: '557:65:0', expected: '-1', returned: '1'}, + { type: 'testPass', debugTxHash: '0xf0721b28c547c1c64948661d677cf6afc10d139315726280162a984f2f7e5d9c', value: 'Lesser than uint int pass test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', context: 'AssertLesserThanTest' }, + { type: 'testFailure', debugTxHash: '0x0757289229b58043c101cb311df8db16d1b30944747493e1491daa9aca6aa30e', value: 'Lesser than uint int fail test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', errMsg: 'lesserThanUintIntFailTest fails', context: 'AssertLesserThanTest', assertMethod: 'lesserThan', location: '826:71:0', expected: '-1', returned: '115792089237316195423570985008687907853269984665640564039457584007913129639935'}, + { type: 'testPass', debugTxHash: '0x316feb8f80c04b12194262dd80b6b004232eab00d7f7c3846badf6e92684e177', value: 'Lesser than int uint pass test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', context: 'AssertLesserThanTest' }, + { type: 'testFailure', debugTxHash: '0x65c5ab3cb85f163eefe3321cc4444defa99154d3cbe415b9384bbd2627449b6a', value: 'Lesser than int uint fail test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', errMsg: 'lesserThanIntUintFailTest fails', context: 'AssertLesserThanTest', assertMethod: 'lesserThan', location: '1105:69:0', expected: '1', returned: '1'}, ], ['time', 'web3']) }) }) @@ -305,10 +305,10 @@ describe('testRunner', () => { deepEqualExcluding(tests, [ { type: 'accountList', value: accounts }, { type: 'contract', value: 'MyTest', filename: __dirname + '/examples_1/simple_storage_test.sol' }, - { type: 'testPass', value: 'Initial value should be100', filename: __dirname + '/examples_1/simple_storage_test.sol', context: 'MyTest' }, - { type: 'testPass', value: 'Initial value should not be200', filename: __dirname + '/examples_1/simple_storage_test.sol', context: 'MyTest' }, - { type: 'testFailure', value: 'Should trigger one fail', filename: __dirname + '/examples_1/simple_storage_test.sol', errMsg: 'uint test 1 fails', context: 'MyTest', assertMethod: 'equal', location: '532:51:1', expected: '2', returned: '1'}, - { type: 'testPass', value: 'Should trigger one pass', filename: __dirname + '/examples_1/simple_storage_test.sol', context: 'MyTest' } + { type: 'testPass', debugTxHash: '0x5a805403a12f0431c5dd190d31a87eb62758f09dddc0c6ee7ee6899c5e7eba71', value: 'Initial value should be100', filename: __dirname + '/examples_1/simple_storage_test.sol', context: 'MyTest' }, + { type: 'testPass', debugTxHash: '0xd0ae7cb5a3a0f5e8f7bf90129e3daba36f649a5c1176ad54609f7b7615bef7dd', value: 'Initial value should not be200', filename: __dirname + '/examples_1/simple_storage_test.sol', context: 'MyTest' }, + { type: 'testFailure', debugTxHash: '0xb0d613434f2fd7060f97d4ca8bbfd8f2deeebed83062a25044f0237bd38b3229', value: 'Should trigger one fail', filename: __dirname + '/examples_1/simple_storage_test.sol', errMsg: 'uint test 1 fails', context: 'MyTest', assertMethod: 'equal', location: '532:51:1', expected: '2', returned: '1'}, + { type: 'testPass', debugTxHash: '0x5ee675ec81b550386b2fdd359ae3530e49dd3e02145e877a4a5f68753ac4e341', value: 'Should trigger one pass', filename: __dirname + '/examples_1/simple_storage_test.sol', context: 'MyTest' } ], ['time', 'web3']) }) }) @@ -336,8 +336,8 @@ describe('testRunner', () => { deepEqualExcluding(tests, [ { type: 'accountList', value: accounts }, { type: 'contract', value: 'MyTest', filename: __dirname + '/examples_2/simple_storage_test.sol' }, - { type: 'testPass', value: 'Initial value should be100', filename: __dirname + '/examples_2/simple_storage_test.sol', context: 'MyTest' }, - { type: 'testPass', value: 'Value is set200', filename: __dirname + '/examples_2/simple_storage_test.sol', context: 'MyTest' } + { type: 'testPass', debugTxHash: '0xa700d29204f1ddb40ef66f151c44387f905d405b6da10380111a751451af2fe1', value: 'Initial value should be100', filename: __dirname + '/examples_2/simple_storage_test.sol', context: 'MyTest' }, + { type: 'testPass', debugTxHash: '0x2c037b78a435e5964615f838ea65f077f3b15d8552d514b3551d0fb87419e444', value: 'Value is set200', filename: __dirname + '/examples_2/simple_storage_test.sol', context: 'MyTest' } ], ['time', 'web3']) }) }) @@ -362,8 +362,8 @@ describe('testRunner', () => { deepEqualExcluding(tests, [ { type: 'accountList', value: accounts }, { type: 'contract', value: 'StringTest', filename: __dirname + '/examples_3/simple_string_test.sol' }, - { type: 'testPass', value: 'Initial value should be hello world', filename: __dirname + '/examples_3/simple_string_test.sol', context: 'StringTest' }, - { type: 'testPass', value: 'Value should not be hello wordl', filename: __dirname + '/examples_3/simple_string_test.sol', context: 'StringTest' } + { type: 'testPass', debugTxHash: '0x4e160dbc81f88d3d87b39d81651c42b0ea8e3aaa10c1a57394467e073bbcb2a4', value: 'Initial value should be hello world', filename: __dirname + '/examples_3/simple_string_test.sol', context: 'StringTest' }, + { type: 'testPass', debugTxHash: '0x47030578c5bcb990d837356430697d061a02813e3322fa3323f6b5f78176eea6', value: 'Value should not be hello wordl', filename: __dirname + '/examples_3/simple_string_test.sol', context: 'StringTest' } ], ['time', 'web3']) }) }) @@ -388,9 +388,9 @@ describe('testRunner', () => { deepEqualExcluding(tests, [ { type: 'accountList', value: accounts }, { type: 'contract', value: 'StorageResolveTest', filename: __dirname + '/examples_5/test/simple_storage_test.sol' }, - { type: 'testPass', value: 'Initial value should be100', filename: __dirname + '/examples_5/test/simple_storage_test.sol', context: 'StorageResolveTest' }, - { type: 'testPass', value: 'Check if even', filename: __dirname + '/examples_5/test/simple_storage_test.sol', context: 'StorageResolveTest' }, - { type: 'testPass', value: 'Check if odd', filename: __dirname + '/examples_5/test/simple_storage_test.sol', context: 'StorageResolveTest' } + { type: 'testPass', debugTxHash: '0x85e901e9160c4a17725d020f030c7cbb020d36da1fda8422d990391df3cbfcbb', value: 'Initial value should be100', filename: __dirname + '/examples_5/test/simple_storage_test.sol', context: 'StorageResolveTest' }, + { type: 'testPass', debugTxHash: '0x1abb2456746b416cddcaf2f3fe960103e740e9772c47a0f1d65d48394facb21a', value: 'Check if even', filename: __dirname + '/examples_5/test/simple_storage_test.sol', context: 'StorageResolveTest' }, + { type: 'testPass', debugTxHash: '0xfcc2332a24d2780390e85a06343fab81c4dc20c12cf5455d746641a9c3e8db03', value: 'Check if odd', filename: __dirname + '/examples_5/test/simple_storage_test.sol', context: 'StorageResolveTest' } ], ['time', 'web3']) }) }) diff --git a/libs/remix-ui/solidity-compiler/src/lib/actions/compiler.ts b/libs/remix-ui/solidity-compiler/src/lib/actions/compiler.ts index 17e58a0052..bc43ed73b1 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/actions/compiler.ts +++ b/libs/remix-ui/solidity-compiler/src/lib/actions/compiler.ts @@ -42,7 +42,6 @@ export const listenToEvents = (compileTabLogic: CompileTabLogic, api) => (dispat api.onContentChanged = () => { dispatch(setEditorMode('contentChanged')) } - compileTabLogic.compiler.event.register('loadingCompiler', () => { dispatch(setCompilerMode('loadingCompiler')) }) diff --git a/libs/remix-ui/terminal/.babelrc b/libs/remix-ui/terminal/.babelrc new file mode 100644 index 0000000000..09d67939cc --- /dev/null +++ b/libs/remix-ui/terminal/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@nrwl/react/babel"], + "plugins": [] +} diff --git a/libs/remix-ui/terminal/.eslintrc b/libs/remix-ui/terminal/.eslintrc new file mode 100644 index 0000000000..dae5c6feeb --- /dev/null +++ b/libs/remix-ui/terminal/.eslintrc @@ -0,0 +1,19 @@ +{ + "env": { + "browser": true, + "es6": true + }, + "extends": "../../../.eslintrc", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 11, + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "error" + } +} diff --git a/libs/remix-ui/terminal/README.md b/libs/remix-ui/terminal/README.md new file mode 100644 index 0000000000..4638096417 --- /dev/null +++ b/libs/remix-ui/terminal/README.md @@ -0,0 +1,7 @@ +# remix-ui-terminal + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test remix-ui-terminal` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/remix-ui/terminal/package.json b/libs/remix-ui/terminal/package.json new file mode 100644 index 0000000000..623cf89c25 --- /dev/null +++ b/libs/remix-ui/terminal/package.json @@ -0,0 +1,4 @@ +{ + "name": "remix-ui-terminal", + "version": "0.0.1" +} diff --git a/libs/remix-ui/terminal/src/index.ts b/libs/remix-ui/terminal/src/index.ts new file mode 100644 index 0000000000..5b8eeca5b5 --- /dev/null +++ b/libs/remix-ui/terminal/src/index.ts @@ -0,0 +1 @@ +export * from './lib/remix-ui-terminal' diff --git a/libs/remix-ui/terminal/src/lib/actions/terminalAction.ts b/libs/remix-ui/terminal/src/lib/actions/terminalAction.ts new file mode 100644 index 0000000000..dafe6f78cf --- /dev/null +++ b/libs/remix-ui/terminal/src/lib/actions/terminalAction.ts @@ -0,0 +1,152 @@ +import React from 'react' +import { EMPTY_BLOCK, KNOWN_TRANSACTION, NEW_BLOCK, NEW_CALL, NEW_TRANSACTION, UNKNOWN_TRANSACTION } from '../types/terminalTypes' + +export const registerCommandAction = (name: string, command, activate, dispatch: React.Dispatch) => { + const commands: any = {} + const _commands: any = {} + _commands[name] = command + const data: any = { + session: [], + activeFilters: { commands: {}, input: '' }, + filterFns: {} + } + const _INDEX = { + all: [], + allMain: [], + commands: {}, + commandsMain: {} + } + + const registerFilter = (commandName, filterFn) => { + data.filterFns[commandName] = filterFn + } + + commands[name] = function () { + const args = [...arguments] + const steps = [] + const root = { steps, cmd: name, gidx: 0, idx: 0 } + const ITEM = { root, cmd: name } + root.gidx = _INDEX.allMain.push(ITEM) - 1 + let item + function append (cmd, params, el) { + if (cmd) { // subcommand + item = { el, cmd, root } + } else { // command + item = ITEM + item.el = el + cmd = name + } + item.gidx = _INDEX.all.push(item) - 1 + item.idx = _INDEX.commands[cmd].push(item) - 1 + item.step = steps.push(item) - 1 + item.args = params + } + const scopedCommands = _scopeCommands(append) + command(args, scopedCommands, el => append(null, args, el)) + } + const help = typeof command.help === 'string' ? command.help : [ + '// no help available for:', `terminal.command.${name}` + ].join('\n') + commands[name].toString = () => { return help } + commands[name].help = help + data.activeFilters.commands[name] = activate && activate.activate + if (activate.filterFn) { + registerFilter(name, activate.filterFn) + } + if (name !== (KNOWN_TRANSACTION || UNKNOWN_TRANSACTION || EMPTY_BLOCK)) { + dispatch({ type: name, payload: { commands: commands, _commands: _commands, data: data } }) + } + + const _scopeCommands = (append) => { + const scopedCommands = {} + Object.keys(commands).forEach(function makeScopedCommand (cmd) { + const command = _commands[cmd] + scopedCommands[cmd] = function _command () { + const args = [...arguments] + command(args, scopedCommands, el => append(cmd, args, el)) + } + }) + return scopedCommands + } +} + +export const filterFnAction = (name: string, filterFn, dispatch: React.Dispatch) => { + const data: any = { + filterFns: {} + } + data.filterFns[name] = filterFn + dispatch({ type: name, payload: { data: data } }) +} + +export const registerLogScriptRunnerAction = (on, commandName, commandFn, dispatch: React.Dispatch) => { + on('scriptRunner', commandName, (msg) => { + commandFn.log.apply(commandFn, msg.data) + dispatch({ type: commandName, payload: { commandFn, message: msg.data } }) + }) +} + +export const registerInfoScriptRunnerAction = (on, commandName, commandFn, dispatch: React.Dispatch) => { + on('scriptRunner', commandName, (msg) => { + commandFn.info.apply(commandFn, msg.data) + dispatch({ type: commandName, payload: { commandFn, message: msg.data } }) + }) +} + +export const registerWarnScriptRunnerAction = (on, commandName, commandFn, dispatch: React.Dispatch) => { + on('scriptRunner', commandName, (msg) => { + commandFn.warn.apply(commandFn, msg.data) + dispatch({ type: commandName, payload: { commandFn, message: msg.data } }) + }) +} + +export const registerErrorScriptRunnerAction = (on, commandName, commandFn, dispatch: React.Dispatch) => { + on('scriptRunner', commandName, (msg) => { + commandFn.error.apply(commandFn, msg.data) + dispatch({ type: commandName, payload: { commandFn, message: msg.data } }) + }) +} + +export const listenOnNetworkAction = async (event, isListening) => { + event.trigger('listenOnNetWork', [isListening]) +} + +export const initListeningOnNetwork = (plugins, dispatch: React.Dispatch) => { + plugins.txListener.event.register(NEW_BLOCK, (block) => { + if (!block.transactions || (block.transactions && !block.transactions.length)) { + dispatch({ type: EMPTY_BLOCK, payload: { message: 0 } }) + } + }) + plugins.txListener.event.register(KNOWN_TRANSACTION, () => { + }) + plugins.txListener.event.register(NEW_CALL, (tx, receipt) => { + log(plugins, tx, receipt, dispatch) + // log(this, tx, null) + }) + plugins.txListener.event.register(NEW_TRANSACTION, (tx, receipt) => { + log(plugins, tx, receipt, dispatch) + }) + + const log = async (plugins, tx, receipt, dispatch: React.Dispatch) => { + const resolvedTransaction = await plugins.txListener.resolvedTransaction(tx.hash) + if (resolvedTransaction) { + let compiledContracts = null + if (plugins._deps.compilersArtefacts.__last) { + compiledContracts = await plugins._deps.compilersArtefacts.__last.getContracts() + } + await plugins.eventsDecoder.parseLogs(tx, resolvedTransaction.contractName, compiledContracts, async (error, logs) => { + if (!error) { + await dispatch({ type: KNOWN_TRANSACTION, payload: { message: [{ tx: tx, receipt: receipt, resolvedData: resolvedTransaction, logs: logs }] } }) + } + }) + } else { + await dispatch({ type: UNKNOWN_TRANSACTION, payload: { message: [{ tx: tx, receipt: receipt }] } }) + } + } + + plugins.txListener.event.register('debuggingRequested', async (hash) => { + // TODO should probably be in the run module + if (!await plugins.options.appManager.isActive('debugger')) await plugins.options.appManager.activatePlugin('debugger') + plugins.call('menuicons', 'select', 'debugger') + plugins.call('debugger', 'debug', hash) + }) +} diff --git a/libs/remix-ui/terminal/src/lib/commands.ts b/libs/remix-ui/terminal/src/lib/commands.ts new file mode 100644 index 0000000000..cc3b5e6963 --- /dev/null +++ b/libs/remix-ui/terminal/src/lib/commands.ts @@ -0,0 +1,53 @@ +export const allPrograms = [ + { ethers: 'The ethers.js library is a compact and complete JavaScript library for Ethereum.' }, + { remix: 'Ethereum IDE and tools for the web.' }, + { web3: 'The web3.js library is a collection of modules which contain specific functionality for the ethereum ecosystem.' } + // { swarmgw: 'This library can be used to upload/download files to Swarm via https://swarm-gateways.net/.' } +] + +export const allCommands = [ + { 'remix.execute(filepath)': 'Run the script specified by file path. If filepath is empty, script currently displayed in the editor is executed.' }, + { 'remix.exeCurrent()': 'Run the script currently displayed in the editor.' }, + // { 'remix.help()': 'Display this help message.' }, + { 'remix.loadgist(id)': 'Load a gist in the file explorer.' }, + // { 'remix.loadurl(url)': 'Load the given url in the file explorer. The url can be of type github, swarm or ipfs.' }, + + // { 'swarmgw.get(url, cb)': 'Download files from Swarm via https://swarm-gateways.net/' }, + // { 'swarmgw.put(content, cb)': 'Upload files to Swarm via https://swarm-gateways.net/' }, + + { 'ethers.Contract': 'This API provides a graceful connection to a contract deployed on the blockchain, simplifying calling and querying its functions and handling all the binary protocol and conversion as necessarily.' }, + // { 'ethers.HDNode': 'A Hierarchical Deterministic Wallet represents a large tree of private keys which can reliably be reproduced from an initial seed.' }, + // { 'ethers.Interface': 'The Interface Object is a meta-class that accepts a Solidity (or compatible) Application Binary Interface (ABI) and populates functions to deal with encoding and decoding the parameters to pass in and results returned.' }, + { 'ethers.providers': 'A Provider abstracts a connection to the Ethereum blockchain, for issuing queries and sending state changing transactions.' }, + // { 'ethers.SigningKey': 'The SigningKey interface provides an abstraction around the secp256k1 elliptic curve cryptography library.' }, + // { 'ethers.utils': 'The utility functions exposed in both the ethers umbrella package and the ethers-utils.' }, + // { 'ethers.utils.AbiCoder': 'Create a new ABI Coder object' }, + // { 'ethers.utils.RLP': 'This encoding method is used internally for several aspects of Ethereum, such as encoding transactions and determining contract addresses.' }, + { 'ethers.Wallet': 'A wallet manages a private/public key pair which is used to cryptographically sign transactions and prove ownership on the Ethereum network.' }, + { 'ethers.version': 'Contains the version of the ethers container object.' }, + + { 'web3.eth': 'Eth module for interacting with the Ethereum network.' }, + { 'web3.eth.accounts': 'The web3.eth.accounts contains functions to generate Ethereum accounts and sign transactions and data.' }, + // TODO: need to break down the object return from abi response + // { 'web3.eth.abi': 'The web3.eth.abi functions let you de- and encode parameters to ABI (Application Binary Interface) for function calls to the EVM (Ethereum Virtual Machine).' }, + { 'web3.eth.ens': 'The web3.eth.ens functions let you interacting with ENS.' }, + { 'web3.eth.Iban': 'The web3.eth.Iban function lets convert Ethereum addresses from and to IBAN and BBAN.' }, + { 'web3.eth.net': 'Net module for interacting with network properties.' }, + { 'web3.eth.personal': 'Personal module for interacting with the Ethereum accounts.' }, + { 'web3.eth.subscribe': 'The web3.eth.subscribe function lets you subscribe to specific events in the blockchain.' }, + { 'web3.givenProvider': 'When using web3.js in an Ethereum compatible browser, it will set with the current native provider by that browser. Will return the given provider by the (browser) environment, otherwise null.' }, + // { 'web3.modules': 'Contains the version of the web3 container object.' }, + { 'web3.providers': 'Contains the current available providers.' }, + { 'web3.shh': 'Shh module for interacting with the whisper protocol' }, + { 'web3.utils': 'This package provides utility functions for Ethereum dapps and other web3.js packages.' }, + { 'web3.version': 'Contains the version of the web3 container object.' }, + + { 'web3.eth.clearSubscriptions();': 'Resets subscriptions.' } +// { 'web3.eth.Contract(jsonInterface[, address][, options])': 'The web3.eth.Contract object makes it easy to interact with smart contracts on the ethereum blockchain.' }, +// { 'web3.eth.accounts.create([entropy]);': 'The web3.eth.accounts contains functions to generate Ethereum accounts and sign transactions and data.' }, +// { 'web3.eth.getAccounts();': 'Retrieve the list of accounts' }, +// { 'web3.eth.accounts.privateKeyToAccount(privateKey [, ignoreLength ]);': 'Get the account from the private key' }, +// { 'web3.eth.accounts.signTransaction(tx, privateKey [, callback]);': 'Sign Transaction' }, +// { 'web3.eth.accounts.recoverTransaction(rawTransaction);': 'Sign Transaction' }, +// { 'web3.eth.accounts.hashMessage(message);': 'Hash message' } +] diff --git a/libs/remix-ui/terminal/src/lib/components/ChechTxStatus.tsx b/libs/remix-ui/terminal/src/lib/components/ChechTxStatus.tsx new file mode 100644 index 0000000000..005be81e02 --- /dev/null +++ b/libs/remix-ui/terminal/src/lib/components/ChechTxStatus.tsx @@ -0,0 +1,16 @@ +import React from 'react' // eslint-disable-line + +const CheckTxStatus = ({ tx, type }) => { + if (tx.status === '0x1' || tx.status === true) { + return () + } + if (type === 'call' || type === 'unknownCall' || type === 'unknown') { + return (call) + } else if (tx.status === '0x0' || tx.status === false) { + return () + } else { + return () + } +} + +export default CheckTxStatus diff --git a/libs/remix-ui/terminal/src/lib/components/Context.tsx b/libs/remix-ui/terminal/src/lib/components/Context.tsx new file mode 100644 index 0000000000..a354c4cab4 --- /dev/null +++ b/libs/remix-ui/terminal/src/lib/components/Context.tsx @@ -0,0 +1,62 @@ +import React from 'react' // eslint-disable-line +import helper from 'apps/remix-ide/src/lib/helper' + +const remixLib = require('@remix-project/remix-lib') +const typeConversion = remixLib.execution.typeConversion + +const Context = ({ opts, blockchain }) => { + const data = opts.tx || '' + const from = opts.from ? helper.shortenHexData(opts.from) : '' + let to = opts.to + if (data.to) to = to + ' ' + helper.shortenHexData(data.to) + const val = data.value + let hash = data.hash ? helper.shortenHexData(data.hash) : '' + const input = data.input ? helper.shortenHexData(data.input) : '' + const logs = data.logs && data.logs.decoded && data.logs.decoded.length ? data.logs.decoded.length : 0 + const block = data.receipt ? data.receipt.blockNumber : data.blockNumber || '' + const i = data.receipt ? data.transactionIndex : data.transactionIndex + const value = val ? typeConversion.toInt(val) : 0 + if (blockchain.getProvider() === 'vm') { + return ( +
+ + [vm] +
from: {from}
+
to: {to}
+
value: {value} wei
+
data: {input}
+
logs: {logs}
+
hash: {hash}
+
+
) + } else if (blockchain.getProvider() !== 'vm' && data.resolvedData) { + return ( +
+ + [block:{block} txIndex:{i}] +
from: {from}
+
to: {to}
+
value: {value} wei
+
data: {input}
+
logs: {logs}
+
hash: {hash}
+
+
) + } else { + hash = helper.shortenHexData(data.blockHash) + return ( +
+ + [block:{block} txIndex:{i}] +
from: {from}
+
to: {to}
+
value: {value} wei
+
data: {input}
+
logs: {logs}
+
hash: {hash}
+
+
) + } +} + +export default Context diff --git a/libs/remix-ui/terminal/src/lib/components/RenderCall.tsx b/libs/remix-ui/terminal/src/lib/components/RenderCall.tsx new file mode 100644 index 0000000000..3e94f1e3aa --- /dev/null +++ b/libs/remix-ui/terminal/src/lib/components/RenderCall.tsx @@ -0,0 +1,61 @@ +import React, { useState } from 'react' // eslint-disable-line + +import helper from 'apps/remix-ide/src/lib/helper' +import CheckTxStatus from './ChechTxStatus' // eslint-disable-line +import showTable from './Table' +import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line + +const remixLib = require('@remix-project/remix-lib') +const typeConversion = remixLib.execution.typeConversion + +const RenderCall = ({ tx, resolvedData, logs, index, plugin, showTableHash, txDetails, modal }) => { + const to = resolvedData.contractName + '.' + resolvedData.fn + const from = tx.from ? tx.from : ' - ' + const input = tx.input ? helper.shortenHexData(tx.input) : '' + const txType = 'call' + + const debug = (event, tx) => { + event.stopPropagation() + if (tx.isCall && tx.envMode !== 'vm') { + modal('VM mode', 'Cannot debug this call. Debugging calls is only possible in JavaScript VM mode.', 'Ok', true, () => {}, 'Cancel', () => {}) + } else { + plugin.event.trigger('debuggingRequested', [tx.hash]) + } + } + + return ( + +
txDetails(event, tx)}> + + + [call] +
from: {from}
+
to: {to}
+
data: {input}
+
+
+
debug(event, tx)}>Debug
+
+ +
+ {showTableHash.includes(tx.hash) ? showTable({ + hash: tx.hash, + isCall: tx.isCall, + contractAddress: tx.contractAddress, + data: tx, + from, + to, + gas: tx.gas, + input: tx.input, + 'decoded input': resolvedData && resolvedData.params ? JSON.stringify(typeConversion.stringify(resolvedData.params), null, '\t') : ' - ', + 'decoded output': resolvedData && resolvedData.decodedReturnValue ? JSON.stringify(typeConversion.stringify(resolvedData.decodedReturnValue), null, '\t') : ' - ', + val: tx.value, + logs: logs, + transactionCost: tx.transactionCost, + executionCost: tx.executionCost + }, showTableHash) : null} +
+ ) +} + +export default RenderCall diff --git a/libs/remix-ui/terminal/src/lib/components/RenderKnownTransactions.tsx b/libs/remix-ui/terminal/src/lib/components/RenderKnownTransactions.tsx new file mode 100644 index 0000000000..79e1ba4c8a --- /dev/null +++ b/libs/remix-ui/terminal/src/lib/components/RenderKnownTransactions.tsx @@ -0,0 +1,56 @@ + +import React, { useState } from 'react' // eslint-disable-line +import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line +import CheckTxStatus from './ChechTxStatus' // eslint-disable-line +import Context from './Context' // eslint-disable-line +import showTable from './Table' + +const remixLib = require('@remix-project/remix-lib') +const typeConversion = remixLib.execution.typeConversion + +const RenderKnownTransactions = ({ tx, receipt, resolvedData, logs, index, plugin, showTableHash, txDetails, modal }) => { + const debug = (event, tx) => { + event.stopPropagation() + if (tx.isCall && tx.envMode !== 'vm') { + modal('VM mode', 'Cannot debug this call. Debugging calls is only possible in JavaScript VM mode.', 'Ok', true, () => {}, 'Cancel', () => {}) + } else { + plugin.event.trigger('debuggingRequested', [tx.hash]) + } + } + + const from = tx.from + const to = resolvedData.contractName + '.' + resolvedData.fn + const txType = 'knownTx' + const options = { from, to, tx } + return ( + +
txDetails(event, tx)}> + + +
+
debug(event, tx)}>Debug
+
+ +
+ {showTableHash.includes(tx.hash) ? showTable({ + hash: tx.hash, + status: receipt !== null ? receipt.status : null, + isCall: tx.isCall, + contractAddress: tx.contractAddress, + data: tx, + from, + to, + gas: tx.gas, + input: tx.input, + 'decoded input': resolvedData && resolvedData.params ? JSON.stringify(typeConversion.stringify(resolvedData.params), null, '\t') : ' - ', + 'decoded output': resolvedData && resolvedData.decodedReturnValue ? JSON.stringify(typeConversion.stringify(resolvedData.decodedReturnValue), null, '\t') : ' - ', + logs: logs, + val: tx.value, + transactionCost: tx.transactionCost, + executionCost: tx.executionCost + }, showTableHash) : null} +
+ ) +} + +export default RenderKnownTransactions diff --git a/libs/remix-ui/terminal/src/lib/components/RenderUnknownTransactions.tsx b/libs/remix-ui/terminal/src/lib/components/RenderUnknownTransactions.tsx new file mode 100644 index 0000000000..0a8d43835b --- /dev/null +++ b/libs/remix-ui/terminal/src/lib/components/RenderUnknownTransactions.tsx @@ -0,0 +1,50 @@ +import React, { useState } from 'react' // eslint-disable-line +import { ModalDialog } from '@remix-ui/modal-dialog'// eslint-disable-line +import CheckTxStatus from './ChechTxStatus' // eslint-disable-line +import Context from './Context' // eslint-disable-line +import showTable from './Table' + +const RenderUnKnownTransactions = ({ tx, receipt, index, plugin, showTableHash, txDetails, modal }) => { + const debug = (event, tx) => { + event.stopPropagation() + if (tx.isCall && tx.envMode !== 'vm') { + modal('VM mode', 'Cannot debug this call. Debugging calls is only possible in JavaScript VM mode.', 'Ok', true, () => {}, 'Cancel', () => {}) + } else { + plugin.event.trigger('debuggingRequested', [tx.hash]) + } + } + + const from = tx.from + const to = tx.to + const txType = 'unknown' + (tx.isCall ? 'Call' : 'Tx') + const options = { from, to, tx } + return ( + +
txDetails(event, tx)}> + + +
+
debug(event, tx)}>Debug
+
+ +
+ {showTableHash.includes(tx.hash) ? showTable({ + hash: tx.hash, + status: receipt !== null ? receipt.status : null, + isCall: tx.isCall, + contractAddress: tx.contractAddress, + data: tx, + from, + to, + gas: tx.gas, + input: tx.input, + 'decoded output': ' - ', + val: tx.value, + transactionCost: tx.transactionCost, + executionCost: tx.executionCost + }, showTableHash) : null} +
+ ) +} + +export default RenderUnKnownTransactions diff --git a/libs/remix-ui/terminal/src/lib/components/Table.tsx b/libs/remix-ui/terminal/src/lib/components/Table.tsx new file mode 100644 index 0000000000..33e9cabf58 --- /dev/null +++ b/libs/remix-ui/terminal/src/lib/components/Table.tsx @@ -0,0 +1,165 @@ +import React, { useState } from 'react' // eslint-disable-line +import { CopyToClipboard } from '@remix-ui/clipboard' // eslint-disable-line +import helper from 'apps/remix-ide/src/lib/helper' + +const remixLib = require('@remix-project/remix-lib') +const typeConversion = remixLib.execution.typeConversion + +const showTable = (opts, showTableHash) => { + let msg = '' + let toHash + const data = opts.data // opts.data = data.tx + if (data.to) { + toHash = opts.to + ' ' + data.to + } else { + toHash = opts.to + } + let callWarning = '' + if (opts.isCall) { + callWarning = '(Cost only applies when called by a contract)' + } + if (!opts.isCall) { + if (opts.status !== undefined && opts.status !== null) { + if (opts.status === '0x0' || opts.status === false) { + msg = 'Transaction mined but execution failed' + } else if (opts.status === '0x1' || opts.status === true) { + msg = 'Transaction mined and execution succeed' + } + } else { + msg = 'Status not available at the moment' + } + } + + let stringified = ' - ' + if (opts.logs && opts.logs.decoded) { + stringified = typeConversion.stringify(opts.logs.decoded) + } + const val = opts.val != null ? typeConversion.toInt(opts.val) : 0 + return ( + + + + + + + {opts.hash ? ( + + + ) : null } + { + opts.contractAddress ? ( + + + + + ) : null + } + { + opts.from ? ( + + + + + ) : null + } + { + opts.to ? ( + + + + + ) : null + } + { + opts.gas ? ( + + + + + ) : null + } + { + opts.transactionCost ? ( + + + + + ) : null + } + { + opts.executionCost ? ( + + + + + ) : null + } + {opts.hash ? ( + + + + + ) : null} + {opts.input ? ( + + + + + ) : null} + {opts['decoded input'] ? ( + + + + + ) : null} + {opts['decoded output'] ? ( + + + + + ) : null} + {opts.logs ? ( + + + + + ) : null} + {opts.val ? ( + + + + + ) : null} + +
status{`${opts.status} ${msg}`}
transaction hash{opts.hash} + +
contract address{opts.contractAddress} + +
from{opts.from} + +
to{toHash} + +
gas{opts.gas} gas + +
transaction cost{opts.transactionCost} gas {callWarning} + +
execution cost{opts.executionCost} gas {callWarning} + +
hash{opts.hash} + +
input{helper.shortenHexData(opts.input)} + +
decoded input{opts['decoded input'].trim()} + +
decoded output{opts['decoded output']} + +
logs + {JSON.stringify(stringified, null, '\t')} + + +
val{val} wei + +
+ ) +} +export default showTable diff --git a/libs/remix-ui/terminal/src/lib/custom-hooks/useKeyPress.tsx b/libs/remix-ui/terminal/src/lib/custom-hooks/useKeyPress.tsx new file mode 100644 index 0000000000..12bbc1cefc --- /dev/null +++ b/libs/remix-ui/terminal/src/lib/custom-hooks/useKeyPress.tsx @@ -0,0 +1,29 @@ +import React, {useEffect, useState} from "react" // eslint-disable-line + +export const useKeyPress = (targetKey: string): boolean => { +// State for keeping track of whether key is pressed + const [keyPressed, setKeyPressed] = useState(false) + // If pressed key is our target key then set to true + function downHandler ({ key }): void { + if (key === targetKey) { + setKeyPressed(true) + } + } + // If released key is our target key then set to false + const upHandler = ({ key }): void => { + if (key === targetKey) { + setKeyPressed(false) + } + } + // Add event listeners + useEffect(() => { + window.addEventListener('keydown', downHandler) + window.addEventListener('keyup', upHandler) + // Remove event listeners on cleanup + return () => { + window.removeEventListener('keydown', downHandler) + window.removeEventListener('keyup', upHandler) + } + }, []) // Empty array ensures that effect is only run on mount and unmount + return keyPressed +} diff --git a/libs/remix-ui/terminal/src/lib/reducers/terminalReducer.ts b/libs/remix-ui/terminal/src/lib/reducers/terminalReducer.ts new file mode 100644 index 0000000000..7568737eae --- /dev/null +++ b/libs/remix-ui/terminal/src/lib/reducers/terminalReducer.ts @@ -0,0 +1,195 @@ +import { CLEAR_CONSOLE, CMD_HISTORY, EMPTY_BLOCK, ERROR, HTML, INFO, KNOWN_TRANSACTION, LISTEN_ON_NETWORK, LOG, NEW_TRANSACTION, SCRIPT, UNKNOWN_TRANSACTION, WARN } from '../types/terminalTypes' + +export const initialState = { + journalBlocks: [ + ], + data: { + // lineLength: props.options.lineLength || 80, + session: [], + activeFilters: { commands: {}, input: '' }, + filterFns: {} + }, + _commandHistory: [], + _commands: {}, + commands: {}, + _JOURNAL: [], + _jobs: [], + _INDEX: { + }, + _INDEXall: [], + _INDEXallMain: [], + _INDEXcommands: {}, + _INDEXcommandsMain: {}, + message: [] +} + +export const registerCommandReducer = (state, action) => { + switch (action.type) { + case HTML : + return { + ...state, + _commands: Object.assign(initialState._commands, action.payload._commands), + commands: Object.assign(initialState.commands, action.payload.commands), + data: Object.assign(initialState.data, { ...action.payload.data }) + } + case LOG: + return { + ...state, + _commands: Object.assign(initialState._commands, action.payload._commands), + commands: Object.assign(initialState.commands, action.payload.commands), + data: Object.assign(initialState.data, { ...action.payload.data }) + + } + case INFO: + return { + ...state, + _commands: Object.assign(initialState._commands, action.payload._commands), + commands: Object.assign(initialState.commands, action.payload.commands), + data: Object.assign(initialState.data, action.payload.data) + } + case WARN: + return { + ...state, + _commands: Object.assign(initialState._commands, action.payload._commands), + commands: Object.assign(initialState.commands, action.payload.commands), + data: Object.assign(initialState.data, action.payload.data) + } + case ERROR: + return { + ...state, + _commands: Object.assign(initialState._commands, action.payload._commands), + commands: Object.assign(initialState.commands, action.payload.commands), + data: Object.assign(initialState.data, action.payload.data) + } + case SCRIPT: + return { + ...state, + _commands: Object.assign(initialState._commands, action.payload._commands), + commands: Object.assign(initialState.commands, action.payload.commands), + data: Object.assign(initialState.data, action.payload.data) + } + case CLEAR_CONSOLE: + return { + ...state, + ...state.journalBlocks.splice(0) + } + case LISTEN_ON_NETWORK: + return { + ...state, + journalBlocks: initialState.journalBlocks.push({ message: action.payload.message, style: 'text-info' }) + } + default : + return { state } + } +} + +export const registerFilterReducer = (state, action) => { + switch (action.type) { + case LOG: + return { + ...state, + data: Object.assign(initialState.data.filterFns, action.payload.data.filterFns) + + } + case INFO: + return { + ...state, + data: Object.assign(initialState.data.filterFns, action.payload.data.filterFns) + } + case WARN: + return { + ...state, + data: Object.assign(initialState.data.filterFns, action.payload.data.filterFns) + } + case ERROR: + return { + ...state, + data: Object.assign(initialState.data.filterFns, action.payload.data.filterFns) + } + case SCRIPT: + return { + ...state, + data: Object.assign(initialState.data.filterFns, action.payload.data.filterFns) + } + default : + return { state } + } +} + +export const addCommandHistoryReducer = (state, action) => { + switch (action.type) { + case CMD_HISTORY: + return { + ...state, + _commandHistory: initialState._commandHistory.unshift(action.payload.script) + + } + default : + return { state } + } +} + +export const remixWelcomeTextReducer = (state, action) => { + switch (action.type) { + case 'welcomeText' : + return { + ...state, + journalBlocks: initialState.journalBlocks.push(action.payload.welcomeText) + } + } +} + +export const registerScriptRunnerReducer = (state, action) => { + switch (action.type) { + case HTML: + return { + ...state, + journalBlocks: initialState.journalBlocks.push({ message: action.payload.message, style: 'text-log' }) + } + case LOG: + return { + ...state, + journalBlocks: initialState.journalBlocks.push({ message: action.payload.message, style: 'text-info' }) + } + case INFO: + return { + ...state, + journalBlocks: initialState.journalBlocks.push({ message: action.payload.message, style: 'text-info' }) + } + case WARN: + return { + ...state, + journalBlocks: initialState.journalBlocks.push({ message: action.payload.message, style: 'text-warning' }) + } + case ERROR: + return { + ...state, + journalBlocks: initialState.journalBlocks.push({ message: action.payload.message, style: 'text-danger' }) + } + case SCRIPT: + return { + ...state, + journalBlocks: initialState.journalBlocks.push({ message: action.payload.message, style: 'text-log' }) + } + case KNOWN_TRANSACTION: + return { + ...state, + journalBlocks: initialState.journalBlocks.push({ message: action.payload.message, style: '', name: 'knownTransaction' }) + } + case UNKNOWN_TRANSACTION: + return { + ...state, + journalBlocks: initialState.journalBlocks.push({ message: action.payload.message, style: '', name: 'unknownTransaction' }) + } + case EMPTY_BLOCK: + return { + ...state, + journalBlocks: initialState.journalBlocks.push({ message: action.payload.message, style: '', name: 'emptyBlock' }) + } + case NEW_TRANSACTION: + return { + ...state, + journalBlocks: initialState.journalBlocks.push({ message: action.payload.message, style: '' }) + } + } +} diff --git a/libs/remix-ui/terminal/src/lib/remix-ui-terminal.css b/libs/remix-ui/terminal/src/lib/remix-ui-terminal.css new file mode 100644 index 0000000000..6c3d777539 --- /dev/null +++ b/libs/remix-ui/terminal/src/lib/remix-ui-terminal.css @@ -0,0 +1,421 @@ + +element.style { + height: 323px !important; +} +#terminalCliInput{ + width: 95%; + background: transparent; + border: none; + font-weight: bold; + color: #a2a3b4; + border-top-style: hidden; + + + + + border-right-style: hidden; + border-left-style: hidden; + border-bottom-style: hidden; +} +#terminalCliInput:focus { + outline: none; +} + +.border-primary { + border-color: #007aa6!important; +} + +/* seleted option should reflect the theme color */ +.selectedOptions { + background-color: #222336; +} + +.panel { + position : relative; + display : flex; + flex-direction : column; + font-size : 12px; + min-height : 3em; + } + .bar { + display : flex; + z-index : 2; + } + .menu { + position : relative; + display : flex; + align-items : center; + width : 100%; + max-height : 35px; + min-height : 35px; + } + .toggleTerminal { + cursor : pointer; + } + .toggleTerminal:hover { + color : var(--secondary); + } + .terminal_container { + display : flex; + flex-direction : column; + height : 100%; + overflow-y : auto; + font-family : monospace; + margin : 0px; + background-repeat : no-repeat; + background-position : center 15%; + background-size : auto calc(75% - 1.7em); + } + .terminal { + position : relative; + display : flex; + flex-direction : column; + height : 100%; + } + .journal { + margin-top : auto; + font-family : monospace; + } + .block { + word-break : break-word; + white-space : pre-wrap; + line-height : 2ch; + padding : 1ch; + margin-top : 2ch; + } + .block > pre { + max-height : 200px; + } + .cli { + line-height : 1.7em; + font-family : monospace; + padding : .4em; + color : var(--primary); + } + .prompt { + margin-right : 0.5em; + font-family : monospace; + font-weight : bold; + font-size : 14px; + color : lightgray; + } + .input { + word-break : break-word; + outline : none; + font-family : monospace; + } + .search { + display : flex; + align-items : center; + width : 330px; + padding-left : 20px; + height : 100%; + padding-top : 1px; + padding-bottom : 1px; + } + .filter { + height : 80%; + white-space : nowrap; + overflow : hidden; + text-overflow : ellipsis; + } + .searchIcon { + width : 25px; + border-top-left-radius : 3px; + border-bottom-left-radius : 3px; + display : flex; + align-items : center; + justify-content : center; + margin-right : 5px; + } + .listen { + margin-right : 30px; + min-width : 40px; + height : 13px; + display : flex; + align-items : center; + } + .listenLabel { + min-width: 50px; + } + .verticalLine { + border-left : 1px solid var(--secondary); + height : 65%; + } + .dragbarHorizontal { + position : absolute; + top : 0; + height : 0.2em; + right : 0; + left : 0; + cursor : row-resize; + z-index : 999; + } + + .console { + cursor : pointer; + } + + .dragbarHorizontal:hover { + background-color: #007AA6; + border:2px solid #007AA6; + } + .listenOnNetwork { + min-height: auto; + } + .ghostbar { + position : absolute; + height : 6px; + opacity : 0.5; + cursor : row-resize; + z-index : 9999; + left : 0; + right : 0; + } + + + .divider-hitbox { + color: white; + cursor: row-resize; + align-self: stretch; + display: flex; + align-items: center; + padding: 0 1rem; + } + + .ul {margin-left: 0; padding-left: 20px;} + + .popup { + position : absolute; + text-align : left; + width : 95%; + font-family : monospace; + background-color : var(--secondary); + overflow : auto; + padding-bottom : 13px; + z-index : 80; + bottom : 1em; + border-width : 4px; + left : 2em; + } + + .autoCompleteItem { + padding : 4px; + border-radius : 2px; + } + + .popup a { + cursor : pointer; + } + + .listHandlerShow { + display : block; + } + + .listHandlerHide { + display : none; + } + + .listHandlerButtonShow { + position : fixed; + width : 46%; + } + + .pageNumberAlignment { + font-size : 10px; + float : right; + } + + .modalContent { + position : absolute; + margin-left : 20%; + margin-bottom : 32px; + bottom : 0px; + padding : 0; + line-height : 18px; + font-size : 12px; + width : 40%; + box-shadow : 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19); + -webkit-animation-name: animatebottom; + -webkit-animation-duration: 0.4s; + animation-name : animatetop; + animation-duration: 0.4s + } + + @-webkit-keyframes animatetop { + from {bottom: -300px; opacity: 0} + to {bottom: 0; opacity: 1} + } + + @keyframes animatetop { + from {bottom: -300px; opacity: 0} + to {bottom: 0; opacity: 1} + } + + + + + /* tx logger css*/ + + .log { + display: flex; + cursor: pointer; + align-items: center; + cursor: pointer; + } + .log:hover { + opacity: 0.8; + } + + .txStatus { + display: flex; + font-size: 20px; + margin-right: 20px; + float: left; + } + .succeeded { + color: var(--success); + } + .failed { + color: var(--danger); + } + + .terminal_arrow { + color: var(--text-info); + font-size: 20px; + cursor: pointer; + display: flex; + margin-left: 10px; + } + .terminal_arrow:hover { + color: var(--secondary); + } + .notavailable { + } + .call { + font-size: 7px; + border-radius: 50%; + min-width: 20px; + min-height: 20px; + display: flex; + justify-content: center; + align-items: center; + color: var(--text-info); + text-transform: uppercase; + font-weight: bold; + } + .txItem { + color: var(--text-info); + margin-right: 5px; + float: left; + } + .txItemTitle { + font-weight: bold; + } + .tx { + color: var(--text-info); + font-weight: bold; + float: left; + margin-right: 10px; + } + .txTable, + .tr, + .td { + border-collapse: collapse; + font-size: 10px; + color: var(--text-info); + border: 1px solid var(--text-info); + transition: max-height 0.3s, padding 0.3s; + } + table .active { + transition: max-height 0.6s, padding 0.6s; + } + #txTable { + margin-top: 1%; + margin-bottom: 5%; + align-self: center; + width: 85%; + } + .tr, .td { + padding: 4px; + vertical-align: baseline; + } + .td:first-child { + min-width: 30%; + width: 30%; + align-items: baseline; + font-weight: bold; + } + .tableTitle { + width: 25%; + } + .buttons { + display: flex; + margin-left: auto; + } + .debug { + white-space: nowrap; + } + .debug:hover { + opacity: 0.8; + } + + + /* Style the accordion section */ +.accordion__section { + display: flex; + flex-direction: column; +} + +/* Style the buttons that are used to open and close the accordion panel */ +.accordion { + background-color: #eee; + color: #444; + cursor: pointer; + padding: 18px; + display: flex; + align-items: center; + border: none; + outline: none; + transition: background-color 0.6s ease; +} + +/* Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) */ +/* .accordion:hover, +.active { + background-color: #ccc; +} */ + +/* Style the accordion content title */ +.accordion__title { + font-family: "Open Sans", sans-serif; + font-weight: 600; + font-size: 14px; +} + +/* Style the accordion chevron icon */ +.accordion__icon { + margin-left: auto; + transition: transform 0.6s ease; +} + +/* Style to rotate icon when state is active */ +.rotate { + transform: rotate(90deg); +} + +/* Style the accordion content panel. Note: hidden by default */ +.accordion__content { + background-color: white; + overflow: hidden; + transition: max-height 0.6s ease; +} + +/* Style the accordion content text */ +.accordion__text { + font-family: "Open Sans", sans-serif; + font-weight: 400; + font-size: 14px; + padding: 18px; +} + diff --git a/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx b/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx new file mode 100644 index 0000000000..8b729f07d4 --- /dev/null +++ b/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx @@ -0,0 +1,574 @@ +import React, { useState, useEffect, useReducer, useRef, SyntheticEvent, MouseEvent } from 'react' // eslint-disable-line +import { registerCommandAction, registerLogScriptRunnerAction, registerInfoScriptRunnerAction, registerErrorScriptRunnerAction, registerWarnScriptRunnerAction, listenOnNetworkAction, initListeningOnNetwork } from './actions/terminalAction' +import { initialState, registerCommandReducer, addCommandHistoryReducer, registerScriptRunnerReducer } from './reducers/terminalReducer' +import { getKeyOf, getValueOf, Objectfilter, matched } from './utils/utils' +import {allCommands, allPrograms} from './commands' // eslint-disable-line +import TerminalWelcomeMessage from './terminalWelcome' // eslint-disable-line +import { Toaster } from '@remix-ui/toaster' // eslint-disable-line +import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line + +import './remix-ui-terminal.css' +import vm from 'vm' +import javascriptserialize from 'javascript-serialize' +import jsbeautify from 'js-beautify' +import RenderUnKnownTransactions from './components/RenderUnknownTransactions' // eslint-disable-line +import RenderCall from './components/RenderCall' // eslint-disable-line +import RenderKnownTransactions from './components/RenderKnownTransactions' // eslint-disable-line +import parse from 'html-react-parser' +import { EMPTY_BLOCK, KNOWN_TRANSACTION, RemixUiTerminalProps, UNKNOWN_TRANSACTION } from './types/terminalTypes' +import { wrapScript } from './utils/wrapScript' + +/* eslint-disable-next-line */ +export interface ClipboardEvent extends SyntheticEvent { + clipboardData: DataTransfer; +} + +export const RemixUiTerminal = (props: RemixUiTerminalProps) => { + const { call, _deps, on, config, event, gistHandler, logHtmlResponse, logResponse, version } = props.plugin + const [toggleDownUp, setToggleDownUp] = useState('fa-angle-double-down') + const [_cmdIndex, setCmdIndex] = useState(-1) + const [_cmdTemp, setCmdTemp] = useState('') + // dragable state + const [leftHeight, setLeftHeight] = useState(undefined) + const [separatorYPosition, setSeparatorYPosition] = useState(undefined) + const [dragging, setDragging] = useState(false) + + const [newstate, dispatch] = useReducer(registerCommandReducer, initialState) + const [cmdHistory, cmdHistoryDispatch] = useReducer(addCommandHistoryReducer, initialState) + const [, scriptRunnerDispatch] = useReducer(registerScriptRunnerReducer, initialState) + const [toaster, setToaster] = useState(false) + const [toastProvider, setToastProvider] = useState({ show: false, fileName: '' }) + const [modalState, setModalState] = useState({ + message: '', + title: '', + okLabel: '', + cancelLabel: '', + hide: true, + cancelFn: () => {}, + handleHide: () => {} + }) + + const [clearConsole, setClearConsole] = useState(false) + const [paste, setPaste] = useState(false) + const [autoCompletState, setAutoCompleteState] = useState({ + activeSuggestion: 0, + data: { + _options: [] + }, + _startingElement: 0, + autoCompleteSelectedItem: {}, + _elementToShow: 4, + _selectedElement: 0, + filteredCommands: [], + filteredPrograms: [], + showSuggestions: false, + text: '', + userInput: '', + extraCommands: [], + commandHistoryIndex: 0 + }) + + const [searchInput, setSearchInput] = useState('') + const [showTableHash, setShowTableHash] = useState([]) + + // terminal inputRef + const inputEl = useRef(null) + const messagesEndRef = useRef(null) + + // terminal dragable + const leftRef = useRef(null) + const panelRef = useRef(null) + + const scrollToBottom = () => { + messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }) + } + + useEffect(() => { + scriptRunnerDispatch({ type: 'html', payload: { message: logHtmlResponse } }) + }, [logHtmlResponse]) + + useEffect(() => { + scriptRunnerDispatch({ type: 'log', payload: { message: logResponse } }) + }, [logResponse]) + + // events + useEffect(() => { + initListeningOnNetwork(props.plugin, scriptRunnerDispatch) + registerLogScriptRunnerAction(on, 'log', newstate.commands, scriptRunnerDispatch) + registerInfoScriptRunnerAction(on, 'info', newstate.commands, scriptRunnerDispatch) + registerWarnScriptRunnerAction(on, 'warn', newstate.commands, scriptRunnerDispatch) + registerErrorScriptRunnerAction(on, 'error', newstate.commands, scriptRunnerDispatch) + registerCommandAction('html', _blocksRenderer('html'), { activate: true }, dispatch) + registerCommandAction('log', _blocksRenderer('log'), { activate: true }, dispatch) + registerCommandAction('info', _blocksRenderer('info'), { activate: true }, dispatch) + registerCommandAction('warn', _blocksRenderer('warn'), { activate: true }, dispatch) + registerCommandAction('error', _blocksRenderer('error'), { activate: true }, dispatch) + + registerCommandAction('script', function execute (args, scopedCommands) { + var script = String(args[0]) + _shell(script, scopedCommands, function (error, output) { + if (error) scriptRunnerDispatch({ type: 'error', payload: { message: error } }) + if (output) scriptRunnerDispatch({ type: 'script', payload: { message: '5' } }) + }) + }, { activate: true }, dispatch) + }, [autoCompletState.text]) + + useEffect(() => { + scrollToBottom() + }, [newstate.journalBlocks.length, logHtmlResponse.length, toaster]) + + function execute (file, cb) { + function _execute (content, cb) { + if (!content) { + setToaster(true) + if (cb) cb() + return + } + newstate.commands.script(content) + } + + if (typeof file === 'undefined') { + const content = _deps.editor.currentContent() + _execute(content, cb) + return + } + + const provider = _deps.fileManager.fileProviderOf(file) + console.log({ provider }) + + if (!provider) { + // toolTip(`provider for path ${file} not found`) + setToastProvider({ show: true, fileName: file }) + if (cb) cb() + return + } + + provider.get(file, (error, content) => { + console.log({ content }) + if (error) { + // toolTip(error) + // TODO: pop up + if (cb) cb() + return + } + + _execute(content, cb) + }) + } + + function loadgist (id, cb) { + gistHandler.loadFromGist({ gist: id }, _deps.fileManager) + if (cb) cb() + } + + const _shell = async (script, scopedCommands, done) => { // default shell + if (script.indexOf('remix:') === 0) { + return done(null, 'This type of command has been deprecated and is not functionning anymore. Please run remix.help() to list available commands.') + } + if (script.indexOf('remix.') === 0) { + // we keep the old feature. This will basically only be called when the command is querying the "remix" object. + // for all the other case, we use the Code Executor plugin + const context = { remix: { exeCurrent: (script: any) => { return execute(undefined, script) }, loadgist: (id: any) => { return loadgist(id, () => {}) }, execute: (fileName, callback) => { return execute(fileName, callback) } } } + try { + const cmds = vm.createContext(context) + const result = vm.runInContext(script, cmds) // eslint-disable-line + console.log({ result }) + return done(null, result) + } catch (error) { + return done(error.message) + } + } + try { + if (script.trim().startsWith('git')) { + // await this.call('git', 'execute', script) code might be used in the future + } else { + await call('scriptRunner', 'execute', script) + } + done() + } catch (error) { + done(error.message || error) + } + } + + const handleMinimizeTerminal = (e) => { + e.preventDefault() + e.stopPropagation() + if (toggleDownUp === 'fa-angle-double-down') { + setToggleDownUp('fa-angle-double-up') + event.trigger('resize', []) + } else { + const terminalTopOffset = config.get('terminal-top-offset') + event.trigger('resize', [terminalTopOffset]) + setToggleDownUp('fa-angle-double-down') + } + } + + const focusinput = () => { + inputEl.current.focus() + } + + const handleKeyDown = (event) => { + const suggestionCount = autoCompletState.activeSuggestion + if (autoCompletState.userInput !== '' && (event.which === 27 || event.which === 8 || event.which === 46)) { + // backspace or any key that should remove the autocompletion + setAutoCompleteState(prevState => ({ ...prevState, showSuggestions: false })) + } + if (autoCompletState.showSuggestions && (event.which === 13 || event.which === 9)) { + if (autoCompletState.userInput.length === 1) { + setAutoCompleteState(prevState => ({ ...prevState, activeSuggestion: 0, showSuggestions: false, userInput: Object.keys(autoCompletState.data._options[0]).toString() })) + } else { + if (autoCompletState.showSuggestions && (event.which === 13 || event.which === 9)) { + setAutoCompleteState(prevState => ({ ...prevState, activeSuggestion: 0, showSuggestions: false, userInput: autoCompletState.data._options[autoCompletState.activeSuggestion] ? Object.keys(autoCompletState.data._options[autoCompletState.activeSuggestion]).toString() : inputEl.current.value })) + } else { + setAutoCompleteState(prevState => ({ ...prevState, activeSuggestion: 0, showSuggestions: false, userInput: autoCompletState.data._options.length === 1 ? Object.keys(autoCompletState.data._options[0]).toString() : inputEl.current.value })) + } + } + } + if (event.which === 13 && !autoCompletState.showSuggestions) { + if (event.ctrlKey) { // + // on enter, append the value in the cli input to the journal + inputEl.current.focus() + } else { // + event.preventDefault() + setCmdIndex(-1) + setCmdTemp('') + const script = autoCompletState.userInput.trim() // inputEl.current.innerText.trim() + if (script.length) { + cmdHistoryDispatch({ type: 'cmdHistory', payload: { script } }) + newstate.commands.script(wrapScript(script)) + } + setAutoCompleteState(prevState => ({ ...prevState, userInput: '' })) + inputEl.current.innerText = '' + inputEl.current.focus() + setAutoCompleteState(prevState => ({ ...prevState, showSuggestions: false })) + } + } else if (newstate._commandHistory.length && event.which === 38 && !autoCompletState.showSuggestions && (autoCompletState.userInput === '')) { + event.preventDefault() + setAutoCompleteState(prevState => ({ ...prevState, userInput: newstate._commandHistory[0] })) + } else if (event.which === 38 && autoCompletState.showSuggestions) { + event.preventDefault() + if (autoCompletState.activeSuggestion === 0) { + return + } + setAutoCompleteState(prevState => ({ ...prevState, activeSuggestion: suggestionCount - 1, userInput: Object.keys(autoCompletState.data._options[autoCompletState.activeSuggestion]).toString() })) + } else if (event.which === 38 && !autoCompletState.showSuggestions) { // + if (cmdHistory.length - 1 > _cmdIndex) { + setCmdIndex(prevState => prevState++) + } + inputEl.current.innerText = cmdHistory[_cmdIndex] + inputEl.current.focus() + } else if (event.which === 40 && autoCompletState.showSuggestions) { + event.preventDefault() + if ((autoCompletState.activeSuggestion + 1) === autoCompletState.data._options.length) { + return + } + setAutoCompleteState(prevState => ({ ...prevState, activeSuggestion: suggestionCount + 1, userInput: Object.keys(autoCompletState.data._options[autoCompletState.activeSuggestion + 1]).toString() })) + } else if (event.which === 40 && !autoCompletState.showSuggestions) { + if (_cmdIndex > -1) { + setCmdIndex(prevState => prevState--) + } + inputEl.current.innerText = _cmdIndex >= 0 ? cmdHistory[_cmdIndex] : _cmdTemp + inputEl.current.focus() + } else { + setCmdTemp(inputEl.current.innerText) + } + } + + /* start of mouse events */ + + const mousedown = (event: MouseEvent) => { + setSeparatorYPosition(event.clientY) + leftRef.current.style.backgroundColor = '#007AA6' + leftRef.current.style.border = '2px solid #007AA6' + setDragging(true) + } + + const onMouseMove: any = (e: MouseEvent) => { + e.preventDefault() + if (dragging && leftHeight && separatorYPosition) { + const newLeftHeight = leftHeight + separatorYPosition - e.clientY + setSeparatorYPosition(e.clientY) + setLeftHeight(newLeftHeight) + event.trigger('resize', [newLeftHeight + 32]) + } + } + + const onMouseUp = () => { + leftRef.current.style.backgroundColor = '' + leftRef.current.style.border = '' + setDragging(false) + } + + /* end of mouse event */ + + useEffect(() => { + document.addEventListener('mousemove', onMouseMove) + document.addEventListener('mouseup', onMouseUp) + + return () => { + document.removeEventListener('mousemove', onMouseMove) + document.removeEventListener('mouseup', onMouseUp) + } + }, [onMouseMove, onMouseUp]) + + React.useEffect(() => { + if (panelRef) { + if (!leftHeight) { + setLeftHeight(panelRef.current.offsetHeight) + return + } + panelRef.current.style.height = `${leftHeight}px` + } + }, [leftHeight, setLeftHeight, panelRef]) + + /* block contents that gets rendered from scriptRunner */ + + const _blocksRenderer = (mode) => { + if (mode === 'html') { + return function logger (args) { + if (args.length) { + return args[0] + } + } + } + mode = { + log: 'text-log', + info: 'text-info', + warn: 'text-warning', + error: 'text-danger' + }[mode] // defaults + + if (mode) { + const filterUndefined = (el) => el !== undefined && el !== null + return function logger (args) { + const types = args.filter(filterUndefined).map(type => type) + const values = javascriptserialize.apply(null, args.filter(filterUndefined)).map(function (val, idx) { + if (typeof args[idx] === 'string') { + const el = document.createElement('div') + el.innerHTML = args[idx].replace(/(\r\n|\n|\r)/gm, '
') + val = el.children.length === 0 ? el.firstChild : el + } + if (types[idx] === 'element') val = jsbeautify.html(val) + return val + }) + if (values.length) { + return values + } + } + } else { + throw new Error('mode is not supported') + } + } + + /* end of block content that gets rendered from script Runner */ + + const handleClearConsole = () => { + setClearConsole(true) + dispatch({ type: 'clearconsole', payload: [] }) + inputEl.current.focus() + } + /* start of autoComplete */ + + const listenOnNetwork = (e: any) => { + const isListening = e.target.checked + // setIsListeningOnNetwork(isListening) + listenOnNetworkAction(event, isListening) + } + + const onChange = (event: any) => { + event.preventDefault() + const inputString = event.target.value + if (matched(allPrograms, inputString) || inputString.includes('.')) { + if (paste) { + setPaste(false) + setAutoCompleteState(prevState => ({ ...prevState, showSuggestions: false, userInput: inputString })) + } else { + setAutoCompleteState(prevState => ({ ...prevState, showSuggestions: true, userInput: inputString })) + } + const textList = inputString.split('.') + if (textList.length === 1) { + setAutoCompleteState(prevState => ({ ...prevState, data: { _options: [] } })) + const result = Objectfilter(allPrograms, autoCompletState.userInput) + setAutoCompleteState(prevState => ({ ...prevState, data: { _options: result } })) + } else { + setAutoCompleteState(prevState => ({ ...prevState, data: { _options: [] } })) + const result = Objectfilter(allCommands, autoCompletState.userInput) + setAutoCompleteState(prevState => ({ ...prevState, data: { _options: result } })) + } + } else { + setAutoCompleteState(prevState => ({ ...prevState, showSuggestions: false, userInput: inputString })) + } + } + + const handleSelect = (event) => { + const suggestionCount = autoCompletState.activeSuggestion + if (event.keyCode === 38) { + if (autoCompletState.activeSuggestion === 0) { + return + } + setAutoCompleteState(prevState => ({ ...prevState, activeSuggestion: suggestionCount - 1 })) + } else if (event.keyCode === 40) { + if (autoCompletState.activeSuggestion - 1 === autoCompletState.data._options.length) { + return + } + setAutoCompleteState(prevState => ({ ...prevState, activeSuggestion: suggestionCount + 1 })) + } + } + + const modal = (title: string, message: string, okLabel: string, hide: boolean, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => { + setModalState(prevState => ({ ...prevState, message, okLabel, okFn, cancelLabel, cancelFn, hide })) + } + + const handleHideModal = () => { + setModalState(prevState => ({ ...prevState, hide: true })) + } + + const txDetails = (event, tx) => { + if (showTableHash.includes(tx.hash)) { + const index = showTableHash.indexOf(tx.hash) + if (index > -1) { + setShowTableHash((prevState) => prevState.filter((x) => x !== tx.hash)) + } + } else { + setShowTableHash((prevState) => ([...prevState, tx.hash])) + } + } + + const handleAutoComplete = () => ( +
0 ? 'block' : 'none' }}> +
+ {autoCompletState.data._options.map((item, index) => { + return ( +
+
+ {getKeyOf(item)} +
+
+ {getValueOf(item)} +
+
+ ) + })} +
+
+ ) + /* end of autoComplete */ + + const handlePaste = () => { + setPaste(true) + setAutoCompleteState(prevState => ({ ...prevState, activeSuggestion: 0, showSuggestions: false })) + } + + return ( +
+
+
+
+ +
+ +
+
0
+
+ + +
+
+ + setSearchInput(event.target.value.trim()) } + type="text" + className="border filter form-control" + id="searchInput" + placeholder="Search with transaction hash or address" + data-id="terminalInputSearch" /> +
+
+
+
+ { + handleAutoComplete() + } +
+
+
+ {!clearConsole && } + {newstate.journalBlocks && newstate.journalBlocks.map((x, index) => { + if (x.name === EMPTY_BLOCK) { + return ( +
+ +
[block:{x.message} - 0 {'transactions'} ]
+
+ ) + } else if (x.name === UNKNOWN_TRANSACTION) { + return x.message.filter(x => x.tx.hash.includes(searchInput) || x.tx.from.includes(searchInput) || (x.tx.to.includes(searchInput))).map((trans) => { + return (
{ }
) + }) + } else if (x.name === KNOWN_TRANSACTION) { + return x.message.map((trans) => { + return (
{ trans.tx.isCall ? : () }
) + }) + } else if (Array.isArray(x.message)) { + return x.message.map((msg, i) => { + if (typeof msg === 'object') { + return ( +
{ msg.value ? parse(msg.value) : JSON.stringify(msg) }
+ ) + } else { + return ( +
{ msg ? msg.toString().replace(/,/g, '') : msg }
+ ) + } + }) + } else { + if (typeof x.message !== 'function') { + return ( +
{x.message}
+ ) + } + } + })} +
+
+
+ {'>'} + onChange(event)} onKeyDown={(event) => handleKeyDown(event) } value={autoCompletState.userInput} onPaste={handlePaste}> +
+
+
+ + {toaster && } + {toastProvider.show && } +
+ ) +} + +export default RemixUiTerminal diff --git a/libs/remix-ui/terminal/src/lib/terminalWelcome.tsx b/libs/remix-ui/terminal/src/lib/terminalWelcome.tsx new file mode 100644 index 0000000000..6f8cca225b --- /dev/null +++ b/libs/remix-ui/terminal/src/lib/terminalWelcome.tsx @@ -0,0 +1,29 @@ +import React from 'react' // eslint-disable-line + +const TerminalWelcomeMessage = ({ packageJson }) => { + return ( +
+
- Welcome to Remix {packageJson} -

+
You can use this terminal to:
+
    +
  • Check transactions details and start debugging.
  • +
  • Execute JavaScript scripts: +
    + - Input a script directly in the command line interface +
    + - Select a Javascript file in the file explorer and then run \`remix.execute()\` or \`remix.exeCurrent()\` in the command line interface +
    + - Right click on a JavaScript file in the file explorer and then click \`Run\` +
  • +
+
The following libraries are accessible:
+ +
+ ) +} + +export default TerminalWelcomeMessage diff --git a/libs/remix-ui/terminal/src/lib/types/terminalTypes.ts b/libs/remix-ui/terminal/src/lib/types/terminalTypes.ts new file mode 100644 index 0000000000..66eea4d655 --- /dev/null +++ b/libs/remix-ui/terminal/src/lib/types/terminalTypes.ts @@ -0,0 +1,28 @@ + +export interface ROOTS { + steps: any, + cmd: string, + gidx: number, + idx: number +} + +export const KNOWN_TRANSACTION = 'knownTransaction' +export const UNKNOWN_TRANSACTION = 'unknownTransaction' +export const EMPTY_BLOCK = 'emptyBlock' +export const NEW_TRANSACTION = 'newTransaction' +export const NEW_BLOCK = 'newBlock' +export const NEW_CALL = 'newCall' + +export const HTML = 'html' +export const LOG = 'log' +export const INFO = 'info' +export const WARN = 'warn' +export const ERROR = 'error' +export const SCRIPT = 'script' +export const CLEAR_CONSOLE = 'clearconsole' +export const LISTEN_ON_NETWORK = 'listenOnNetWork' +export const CMD_HISTORY = 'cmdHistory' + +export interface RemixUiTerminalProps { + plugin: any +} diff --git a/libs/remix-ui/terminal/src/lib/utils/utils.ts b/libs/remix-ui/terminal/src/lib/utils/utils.ts new file mode 100644 index 0000000000..62091ee9c8 --- /dev/null +++ b/libs/remix-ui/terminal/src/lib/utils/utils.ts @@ -0,0 +1,42 @@ + +export const getKeyOf = (item) => { + return Object.keys(item)[0] +} + +export const getValueOf = (item) => { + return Object.values(item)[0] +} + +export const Objectfilter = (obj: any, filterValue: any) => + obj.filter((item: any) => Object.keys(item)[0].indexOf(filterValue) > -1) + +export const matched = (arr, value) => arr.map(x => Object.keys(x).some(x => x.startsWith(value))).some(x => x === true) + +const findDeep = (object, fn, found = { break: false, value: undefined }) => { + if (typeof object !== 'object' || object === null) return + for (var i in object) { + if (found.break) break + var el = object[i] + if (el && el.innerText !== undefined && el.innerText !== null) el = el.innerText + if (fn(el, i, object)) { + found.value = el + found.break = true + break + } else { + findDeep(el, fn, found) + } + } + return found.value +} + +export const find = (args, query) => { + query = query.trim() + const isMatch = !!findDeep(args, function check (value) { + if (value === undefined || value === null) return false + if (typeof value === 'function') return false + if (typeof value === 'object') return false + const contains = String(value).indexOf(query.trim()) !== -1 + return contains + }) + return isMatch +} diff --git a/libs/remix-ui/terminal/src/lib/utils/wrapScript.ts b/libs/remix-ui/terminal/src/lib/utils/wrapScript.ts new file mode 100644 index 0000000000..3d9ddcdeea --- /dev/null +++ b/libs/remix-ui/terminal/src/lib/utils/wrapScript.ts @@ -0,0 +1,16 @@ +export const wrapScript = (script) => { + const isKnownScript = ['remix.', 'git'].some(prefix => script.trim().startsWith(prefix)) + if (isKnownScript) return script + return ` + try { + const ret = ${script}; + if (ret instanceof Promise) { + ret.then((result) => { console.log(result) }).catch((error) => { console.log(error) }) + } else { + console.log(ret) + } + } catch (e) { + console.log(e.message) + } + ` +} diff --git a/libs/remix-ui/terminal/tsconfig.json b/libs/remix-ui/terminal/tsconfig.json new file mode 100644 index 0000000000..d52e31ad74 --- /dev/null +++ b/libs/remix-ui/terminal/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/libs/remix-ui/terminal/tsconfig.lib.json b/libs/remix-ui/terminal/tsconfig.lib.json new file mode 100644 index 0000000000..b560bc4dec --- /dev/null +++ b/libs/remix-ui/terminal/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"] +} diff --git a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx index 88fb39baed..2707079c15 100644 --- a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx @@ -332,7 +332,7 @@ export const Workspace = (props: WorkspaceProps) => { return (
- { handleHide={ handleHideModal }> { (typeof state.modal.message !== 'string') && state.modal.message } + }
resetFocus(true)}>
diff --git a/nx.json b/nx.json index 45d68149ae..e86b587935 100644 --- a/nx.json +++ b/nx.json @@ -121,6 +121,9 @@ "remix-ui-renderer": { "tags": [] }, + "remix-ui-terminal": { + "tags": [] + }, "solidity-compiler": { "tags": [] }, diff --git a/package.json b/package.json index cc17c169f2..fc6bbc53ad 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,8 @@ "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,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-file-explorer,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,remix-ui-plugin-manager", - "build:libs": "nx run-many --target=build --parallel=false --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd", + "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,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-file-explorer,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,remix-ui-plugin-manager,remix-ui-terminal", + "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,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": "npm run build:libs && lerna publish --skip-git && npm run bumpVersion:libs", "build:e2e": "tsc -p apps/remix-ide-e2e/tsconfig.e2e.json", @@ -164,6 +164,7 @@ "file-saver": "^2.0.5", "form-data": "^4.0.0", "fs-extra": "^3.0.1", + "html-react-parser": "^1.3.0", "http-server": "^0.11.1", "intro.js": "^4.1.0", "isbinaryfile": "^3.0.2", diff --git a/tsconfig.base.json b/tsconfig.base.json index 86f477732d..38538c202a 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -43,8 +43,9 @@ "@remix-project/core-plugin": ["libs/remix-core-plugin/src/index.ts"], "@remix-ui/solidity-compiler": ["libs/remix-ui/solidity-compiler/src/index.ts"], "@remix-ui/publish-to-storage": ["libs/remix-ui/publish-to-storage/src/index.ts"], - "@remix-ui/plugin-manager": ["libs/remix-ui/plugin-manager/src/index.ts"], - "@remix-ui/renderer": ["libs/remix-ui/renderer/src/index.ts"] + "@remix-ui/renderer": ["libs/remix-ui/renderer/src/index.ts"], + "@remix-ui/terminal": ["libs/remix-ui/terminal/src/index.ts"], + "@remix-ui/plugin-manager": ["libs/remix-ui/plugin-manager/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] diff --git a/workspace.json b/workspace.json index cc763f7202..8d6c4d0e5c 100644 --- a/workspace.json +++ b/workspace.json @@ -778,9 +778,25 @@ } } }, + "remix-ui-terminal": { + "root": "libs/remix-ui/terminal", + "sourceRoot": "libs/remix-ui/terminal/src", + "projectType": "library", + "schematics": {}, + "architect": { + "lint": { + "builder": "@nrwl/linter:lint", + "options": { + "linter": "eslint", + "tsConfig": ["libs/remix-ui/terminal/tsconfig.lib.json"], + "exclude": ["**/node_modules/**", "!libs/remix-ui/terminal/**/*"] + } + } + } + }, "remix-ui-plugin-manager": { - "root": "libs/remix-ui/plugin-manager", - "sourceRoot": "libs/remix-ui/plugin-manager/src", + "root": "libs/remix-ui/plugin-manager", + "sourceRoot": "libs/remix-ui/plugin-manager/src", "projectType": "library", "schematics": {}, "architect": {