diff --git a/.circleci/config.yml b/.circleci/config.yml index a02ab093d2..67b99229f6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -627,7 +627,7 @@ jobs: remix-ide-browser: docker: - - image: cimg/node:20.0.0-browsers + - image: cimg/node:20.17.0-browsers resource_class: xlarge working_directory: ~/remix-project @@ -640,7 +640,10 @@ jobs: type: string jobsize: type: string - parallelism: 10 + parallelism: + type: integer + default: 1 + parallelism: << parameters.parallelism >> steps: - checkout - attach_workspace: @@ -688,7 +691,7 @@ jobs: remix-test-plugins: docker: - - image: cimg/node:20.0.0-browsers + - image: cimg/node:20.17.0-browsers resource_class: xlarge working_directory: ~/remix-project @@ -781,6 +784,7 @@ workflows: script: ["flaky.sh"] job: ["nogroup"] jobsize: ["1"] + parallelism: [1] build_all: unless: << pipeline.parameters.run_flaky_tests >> jobs: @@ -862,16 +866,33 @@ workflows: requires: - build matrix: + alias: browser-tests parameters: browser: ["chrome", "firefox"] script: ["browser_test.sh"] job: ["0","1","2","3","4","5","6","7","8","9"] jobsize: ["10"] + parallelism: [15] + - remix-ide-browser: + requires: + - build + matrix: + alias: metamask + parameters: + browser: ["chrome"] + script: ["metamask.sh"] + job: ["0"] + jobsize: ["10"] + parallelism: [1] + filters: + branches: + only: [/.*metamask.*/, 'master', 'remix_live', 'remix_beta'] - tests-passed: requires: - lint - remix-libs - - remix-ide-browser + - browser-tests + - metamask - plugins - predeploy: @@ -884,7 +905,8 @@ workflows: requires: - lint - remix-libs - - remix-ide-browser + - browser-tests + - metamask - plugins - predeploy filters: @@ -896,7 +918,8 @@ workflows: requires: - lint - remix-libs - - remix-ide-browser + - browser-tests + - metamask - plugins - predeploy filters: @@ -908,7 +931,8 @@ workflows: requires: - lint - remix-libs - - remix-ide-browser + - browser-tests + - metamask - plugins - predeploy filters: diff --git a/.github/workflows/pr-reminder.yml b/.github/workflows/pr-reminder.yml index 7f2295f7a3..e455d557a7 100644 --- a/.github/workflows/pr-reminder.yml +++ b/.github/workflows/pr-reminder.yml @@ -19,7 +19,7 @@ jobs: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} freeze-date: '2024-11-04T18:00:00Z' - name: Reminder for standup - if: github.event.schedule == '0 9 * * 1-5' + if: github.event.schedule == '58 9 * * 1-5' uses: Aniket-Engg/pr-reviews-reminder-action@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/apps/contract-verification/src/app/components/NavMenu.tsx b/apps/contract-verification/src/app/components/NavMenu.tsx index a949b4da16..d835e1a193 100644 --- a/apps/contract-verification/src/app/components/NavMenu.tsx +++ b/apps/contract-verification/src/app/components/NavMenu.tsx @@ -10,6 +10,7 @@ interface NavItemProps { const NavItem: React.FC = ({ to, icon, title }) => { return ( 'text-decoration-none d-flex px-1 py-1 flex-column justify-content-center small ' + (isActive ? "bg-light border-top border-left border-right" : "border-0 bg-transparent")} > diff --git a/apps/contract-verification/src/app/components/SearchableChainDropdown.tsx b/apps/contract-verification/src/app/components/SearchableChainDropdown.tsx index c6f78c5f2f..281c763023 100644 --- a/apps/contract-verification/src/app/components/SearchableChainDropdown.tsx +++ b/apps/contract-verification/src/app/components/SearchableChainDropdown.tsx @@ -37,7 +37,6 @@ export const SearchableChainDropdown: React.FC = ({ label, id, se const [searchTerm, setSearchTerm] = useState(selectedChain ? getChainDescriptor(selectedChain) : '') const [isOpen, setIsOpen] = useState(false) - const [filteredOptions, setFilteredOptions] = useState(dropdownChains) const dropdownRef = useRef(null) const fuse = new Fuse(dropdownChains, { @@ -45,14 +44,7 @@ export const SearchableChainDropdown: React.FC = ({ label, id, se threshold: 0.3, }) - useEffect(() => { - if (searchTerm === '') { - setFilteredOptions(dropdownChains) - } else { - const result = fuse.search(searchTerm) - setFilteredOptions(result.map(({ item }) => item)) - } - }, [searchTerm, dropdownChains]) + const filteredOptions = searchTerm ? fuse.search(searchTerm).map(({ item }) => item) : dropdownChains // Close dropdown when user clicks outside useEffect(() => { @@ -98,16 +90,14 @@ export const SearchableChainDropdown: React.FC = ({ label, id, se {' '} {/* Add ref here */} - - {isOpen && ( -
    - {filteredOptions.map((chain) => ( -
  • handleOptionClick(chain)} className={`dropdown-item text-dark ${selectedChain?.chainId === chain.chainId ? 'active' : ''}`} style={{ cursor: 'pointer', whiteSpace: 'normal' }}> - {getChainDescriptor(chain)} -
  • - ))} -
- )} + +
    + {filteredOptions.map((chain) => ( +
  • handleOptionClick(chain)} data-id={chain.chainId} className={`dropdown-item text-dark ${selectedChain?.chainId === chain.chainId ? 'active' : ''}`} style={{ cursor: 'pointer', whiteSpace: 'normal' }}> + {getChainDescriptor(chain)} +
  • + ))} +
) } diff --git a/apps/contract-verification/src/app/layouts/Default.tsx b/apps/contract-verification/src/app/layouts/Default.tsx index 4052b9a76b..b02a4a77e3 100644 --- a/apps/contract-verification/src/app/layouts/Default.tsx +++ b/apps/contract-verification/src/app/layouts/Default.tsx @@ -13,7 +13,7 @@ export const DefaultLayout = ({ children, title, description }: PropsWithChildre
-
+

{description}

diff --git a/apps/contract-verification/src/app/views/ReceiptsView.tsx b/apps/contract-verification/src/app/views/ReceiptsView.tsx index 5c7eb7b1fa..c24b63ca6b 100644 --- a/apps/contract-verification/src/app/views/ReceiptsView.tsx +++ b/apps/contract-verification/src/app/views/ReceiptsView.tsx @@ -10,7 +10,7 @@ export const ReceiptsView = () => {
{contracts.length > 0 ? contracts.map((contract, index) => ( - )) :
No contracts submitted for verification
} + )) :
No contracts submitted for verification
}
) } diff --git a/apps/contract-verification/src/app/views/VerifyView.tsx b/apps/contract-verification/src/app/views/VerifyView.tsx index 20a0022d47..a924a2a89a 100644 --- a/apps/contract-verification/src/app/views/VerifyView.tsx +++ b/apps/contract-verification/src/app/views/VerifyView.tsx @@ -68,9 +68,9 @@ export const VerifyView = () => { name: verifierId as VerifierIdentifier, } receipts.push({ verifierInfo, status: 'pending', contractId, isProxyReceipt: false, failedChecks: 0 }) - if (enabledVerifiers.Blockscout) await sendToMatomo('verify', "verifyWith: Blockscout On: " + selectedChain + " IsProxy: " + (hasProxy && !proxyAddress)) - if (enabledVerifiers.Etherscan) await sendToMatomo('verify', "verifyWithEtherscan On: " + selectedChain + " IsProxy: " + (hasProxy && !proxyAddress)) - if (enabledVerifiers.Sourcify) await sendToMatomo('verify', "verifyWithSourcify On: " + selectedChain + " IsProxy: " + (hasProxy && !proxyAddress)) + if (enabledVerifiers.Blockscout) await sendToMatomo('verify', "verifyWith: Blockscout On: " + selectedChain?.chainId + " IsProxy: " + (hasProxy && !proxyAddress)) + if (enabledVerifiers.Etherscan) await sendToMatomo('verify', "verifyWithEtherscan On: " + selectedChain?.chainId + " IsProxy: " + (hasProxy && !proxyAddress)) + if (enabledVerifiers.Sourcify) await sendToMatomo('verify', "verifyWithSourcify On: " + selectedChain?.chainId + " IsProxy: " + (hasProxy && !proxyAddress)) } const newSubmittedContract: SubmittedContract = { diff --git a/apps/remix-dapp/src/utils/txRunner.ts b/apps/remix-dapp/src/utils/txRunner.ts index 6f22ca6c13..1914302c02 100644 --- a/apps/remix-dapp/src/utils/txRunner.ts +++ b/apps/remix-dapp/src/utils/txRunner.ts @@ -267,7 +267,7 @@ export class TxRunner { }; } catch (error: any) { console.log( - `Send transaction failed: ${error.message} . if you use an injected provider, please check it is properly unlocked. ` + `Send transaction failed: ${error.message || error.error} . if you use an injected provider, please check it is properly unlocked. ` ); return { error }; } diff --git a/apps/remix-ide-e2e/nightwatch-chrome.ts b/apps/remix-ide-e2e/nightwatch-chrome.ts index f1b95992eb..235fe625bb 100644 --- a/apps/remix-ide-e2e/nightwatch-chrome.ts +++ b/apps/remix-ide-e2e/nightwatch-chrome.ts @@ -21,7 +21,7 @@ module.exports = { 'default': { globals: { waitForConditionTimeout: 10000, - asyncHookTimeout: 10000000 + asyncHookTimeout: 120000 }, screenshots: { enabled: true, diff --git a/apps/remix-ide-e2e/src/commands/hideMetaMaskPopup.ts b/apps/remix-ide-e2e/src/commands/hideMetaMaskPopup.ts index 234a3a08cd..101ce2b324 100644 --- a/apps/remix-ide-e2e/src/commands/hideMetaMaskPopup.ts +++ b/apps/remix-ide-e2e/src/commands/hideMetaMaskPopup.ts @@ -4,25 +4,22 @@ import EventEmitter from 'events' class HideMetaMaskPopup extends EventEmitter { command(this: NightwatchBrowser) { browser - .pause(5000) - .isVisible({ - selector: 'button[data-testid="popover-close"]', - locateStrategy: 'css selector', - suppressNotFoundErrors: true, - timeout: 2000 - }, (okVisible) => { - console.log('okVisible', okVisible) - if (!okVisible.value) { - console.log('popover not found') - } else { - console.log('popover found... closing') - browser.click('button[data-testid="popover-close"]') - } - }) - .waitForElementNotPresent({ - selector: 'button[data-testid="popover-close"]', - locateStrategy: 'css selector', - timeout: 2000 + .perform((done) => { + browser.execute(function () { + function addStyle(styleString) { + const style = document.createElement('style') + style.textContent = styleString + document.head.append(style) + } + addStyle(` + #popover-content { + display:none !important + } + .popover-container { + display:none !important; + } + `) + }, [], done()) }) .perform((done) => { done() diff --git a/apps/remix-ide-e2e/src/commands/setupMetamask.ts b/apps/remix-ide-e2e/src/commands/setupMetamask.ts index 585473827c..6aef24dab1 100644 --- a/apps/remix-ide-e2e/src/commands/setupMetamask.ts +++ b/apps/remix-ide-e2e/src/commands/setupMetamask.ts @@ -50,26 +50,24 @@ function setupMetaMask(browser: NightwatchBrowser, passphrase: string, password: .click('button[data-testid="pin-extension-next"]') .waitForElementVisible('button[data-testid="pin-extension-done"]') .click('button[data-testid="pin-extension-done"]') - .pause(5000) - .isVisible({ - selector: 'button[data-testid="popover-close"]', - locateStrategy: 'css selector', - suppressNotFoundErrors: true, - timeout: 3000 - }, (okVisible) => { - console.log('okVisible', okVisible) - if (!okVisible.value) { - console.log('popover not found') - } else { - console.log('popover found... closing') - browser.click('button[data-testid="popover-close"]') - } - }) - .waitForElementNotPresent({ - selector: 'button[data-testid="popover-close"]', - locateStrategy: 'css selector', - timeout: 3000 + .perform((done) => { + browser.execute(function () { + function addStyle(styleString) { + const style = document.createElement('style') + style.textContent = styleString + document.head.append(style) + } + addStyle(` + #popover-content { + display:none !important + } + .popover-container { + display:none !important; + } + `) + }, [], done()) }) + .saveScreenshot('./reports/screenshots/metamask.png') .click('[data-testid="network-display"]') .click('.mm-modal-content label.toggle-button--off') // show test networks diff --git a/apps/remix-ide-e2e/src/tests/contract_verification.test.ts b/apps/remix-ide-e2e/src/tests/contract_verification.test.ts new file mode 100644 index 0000000000..42bae1fb0e --- /dev/null +++ b/apps/remix-ide-e2e/src/tests/contract_verification.test.ts @@ -0,0 +1,38 @@ +'use strict' +import { NightwatchBrowser } from 'nightwatch' +import init from '../helpers/init' + +declare global { + interface Window { testplugin: { name: string, url: string }; } +} + +const tests = { + '@disabled': true, + before: function (browser: NightwatchBrowser, done: VoidFunction) { + init(browser, done, null) + }, + + 'Should load contract verification plugin #group1': function (browser: NightwatchBrowser) { + browser + .clickLaunchIcon('pluginManager') + .scrollAndClick('[data-id="pluginManagerComponentActivateButtoncontract-verification"]') + .clickLaunchIcon('contract-verification') + .pause(5000) + .frame(0) + .waitForElementVisible('*[data-id="VerifyDescription"]') + }, + + 'Should select a chain by searching #group1': function (browser: NightwatchBrowser) { + browser + .click('[data-id="chainDropdownbox"]') + .sendKeys('[data-id="chainDropdownbox"]', 's') + .sendKeys('[data-id="chainDropdownbox"]', 'c') + .sendKeys('[data-id="chainDropdownbox"]', 'r') + .click('[data-id="534351"]') + .assert.attributeContains('[data-id="chainDropdownbox"]', 'value', "Scroll Sepolia Testnet (534351)") + } +} + +module.exports = { + ...tests +}; diff --git a/apps/remix-ide-e2e/src/tests/matomo.test.ts b/apps/remix-ide-e2e/src/tests/matomo.test.ts index 85369366bc..74db9e8253 100644 --- a/apps/remix-ide-e2e/src/tests/matomo.test.ts +++ b/apps/remix-ide-e2e/src/tests/matomo.test.ts @@ -433,7 +433,7 @@ module.exports = { .click('[data-id="matomoModal-modal-footer-cancel-react"]') // cancel .waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]') }, - 'verify Matomo events are tracked on app start #group4 #lfaky': function (browser: NightwatchBrowser) { + 'verify Matomo events are tracked on app start #group4': function (browser: NightwatchBrowser) { browser .execute(function () { return (window as any)._paq diff --git a/apps/remix-ide-e2e/src/tests/metamask.test.ts b/apps/remix-ide-e2e/src/tests/metamask.test.ts new file mode 100644 index 0000000000..0da38cadaa --- /dev/null +++ b/apps/remix-ide-e2e/src/tests/metamask.test.ts @@ -0,0 +1,433 @@ +'use strict' +import { NightwatchBrowser } from 'nightwatch' +import init from '../helpers/init' +import examples from '../examples/example-contracts' + +const passphrase = process.env.account_passphrase +const password = process.env.account_password +const extension_id = 'nkbihfbeogaeaoehlefnkodbefgpgknn' +const extension_url = `chrome-extension://${extension_id}/home.html` + +const checkBrowserIsChrome = function (browser: NightwatchBrowser) { + return browser.browserName.indexOf('chrome') > -1 +} + +const checkAlerts = function (browser: NightwatchBrowser) { + browser.isVisible({ + selector: '//*[contains(.,"not have enough")]', + locateStrategy: 'xpath', + suppressNotFoundErrors: true, + timeout: 3000 + }, (okVisible) => { + if (okVisible.value) { + browser.assert.fail('Not enough ETH in test account!!') + browser.end() + } + }) +} + +const localsCheck = { + to: { + value: '0x4B0897B0513FDC7C541B6D9D7E929C4E5364D2DB', + type: 'address' + } +} + +const tests = { + '@disabled': true, + before: function (browser: NightwatchBrowser, done: VoidFunction) { + init(browser, done) + }, + + '@sources': function () { + return sources + }, + + 'Should connect to Sepolia Test Network using MetaMask #group1': function (browser: NightwatchBrowser) { + browser.waitForElementPresent('*[data-id="remixIdeSidePanel"]') + .setupMetamask(passphrase, password) + .useCss().switchBrowserTab(0) + .refreshPage() + .waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) + .click('*[data-id="landingPageStartSolidity"]') + .clickLaunchIcon('udapp') + .switchEnvironment('injected-MetaMask') + .waitForElementPresent('*[data-id="settingsNetworkEnv"]') + .assert.containsText('*[data-id="settingsNetworkEnv"]', 'Sepolia (11155111) network') + .pause(5000) + .switchBrowserWindow(extension_url, 'MetaMask', (browser) => { + browser + .hideMetaMaskPopup() + .waitForElementVisible('*[data-testid="page-container-footer-next"]', 60000) + .click('*[data-testid="page-container-footer-next"]') // this connects the metamask account to remix + .pause(2000) + .waitForElementVisible('*[data-testid="page-container-footer-next"]', 60000) + .click('*[data-testid="page-container-footer-next"]') + }) + .switchBrowserTab(0) // back to remix + }, + + 'Should add a contract file #group1': function (browser: NightwatchBrowser) { + browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]') + .clickLaunchIcon('filePanel') + .addFile('Greet.sol', sources[0]['Greet.sol']) + .clickLaunchIcon('udapp') + .waitForElementVisible('*[data-id="Deploy - transact (not payable)"]', 45000) // wait for the contract to compile + }, + + 'Should deploy contract on Sepolia Test Network using MetaMask #group1': function (browser: NightwatchBrowser) { + browser.clearConsole().waitForElementPresent('*[data-id="runTabSelectAccount"] option', 45000) + .clickLaunchIcon('filePanel') + .openFile('Greet.sol') + .clickLaunchIcon('udapp') + .waitForElementPresent('*[data-id="Deploy - transact (not payable)"]') + .click('*[data-id="Deploy - transact (not payable)"]') + .pause(5000) + .clearConsole() + .perform((done) => { + browser.switchBrowserWindow(extension_url, 'MetaMask', (browser) => { + checkAlerts(browser) + browser + .maximizeWindow() + .hideMetaMaskPopup() + .waitForElementPresent('[data-testid="page-container-footer-next"]') + .click('[data-testid="page-container-footer-next"]') // approve the tx + .switchBrowserTab(0) // back to remix + .waitForElementContainsText('*[data-id="terminalJournal"]', 'view on etherscan', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', 'from: 0x76a...2708f', 60000) + .perform(() => done()) + }) + }) + }, + 'Should run low level interaction (fallback function) on Sepolia Test Network using MetaMask #group1': function (browser: NightwatchBrowser) { + browser.clearConsole().waitForElementPresent('*[data-id="remixIdeSidePanel"]') + .clickInstance(0) + .clearConsole() + .waitForElementPresent('*[data-id="pluginManagerSettingsDeployAndRunLLTxSendTransaction"]') + .click('*[data-id="pluginManagerSettingsDeployAndRunLLTxSendTransaction"]') + .perform((done) => { + browser.switchBrowserWindow(extension_url, 'MetaMask', (browser) => { + browser + .maximizeWindow() + .hideMetaMaskPopup() + .pause(3000) + .scrollAndClick('[data-testid="page-container-footer-next"]') + .pause(2000) + .switchBrowserTab(0) // back to remix + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: "//span[@class='text-log' and contains(., 'transact to HelloWorld.(fallback) pending')]" + }) + .waitForElementContainsText('*[data-id="terminalJournal"]', 'view on etherscan', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', 'from: 0x76a...2708f', 60000) + .perform(() => done()) + }) + }) + }, + 'Should run transaction (greet function) on Sepolia Test Network using MetaMask #group1': function (browser: NightwatchBrowser) { + browser.clearConsole().waitForElementPresent('*[data-id="remixIdeSidePanel"]') + .clearConsole() + .waitForElementPresent('*[data-title="string _message"]') + .setValue('*[data-title="string _message"]', 'test') + .waitForElementVisible('*[data-id="greet - transact (not payable)"]') + .click('*[data-id="greet - transact (not payable)"]') + .perform((done) => { + browser.switchBrowserWindow(extension_url, 'MetaMask', (browser) => { + browser + .maximizeWindow() + .hideMetaMaskPopup() + .pause(3000) + .scrollAndClick('[data-testid="page-container-footer-next"]') + .pause(2000) + .switchBrowserTab(0) // back to remix + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: "//span[@class='text-log' and contains(., 'transact to HelloWorld.greet pending')]" + }) + .waitForElementContainsText('*[data-id="terminalJournal"]', 'view on etherscan', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', 'from: 0x76a...2708f', 60000) + .perform(() => done()) + }) + }) + }, + + 'Should deploy faulty contract on Sepolia Test Network using MetaMask and show error in terminal #group1': function (browser: NightwatchBrowser) { + browser + .clearConsole() + .clickLaunchIcon('filePanel') + .addFile('faulty.sol', sources[0]['faulty.sol']) + .clickLaunchIcon('udapp') + .waitForElementPresent('*[data-id="Deploy - transact (not payable)"]') + .click('*[data-id="Deploy - transact (not payable)"]') + .pause(5000) + .waitForElementVisible('*[data-id="udappNotifyModalDialogModalBody-react"]', 60000) + .click('[data-id="udappNotify-modal-footer-cancel-react"]') + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: "//span[@class='text-log' and contains(., 'errored')]" + }) + }, + 'Should deploy contract on Sepolia Test Network using MetaMask again #group1': function (browser: NightwatchBrowser) { + browser.clearConsole().waitForElementPresent('*[data-id="runTabSelectAccount"] option', 45000) + .clickLaunchIcon('filePanel') + .openFile('Greet.sol') + .clickLaunchIcon('udapp') + .waitForElementPresent('*[data-id="Deploy - transact (not payable)"]') + .click('*[data-id="Deploy - transact (not payable)"]') + .pause(5000) + .clearConsole() + .perform((done) => { + browser.switchBrowserWindow(extension_url, 'MetaMask', (browser) => { + checkAlerts(browser) + browser + .maximizeWindow() + .hideMetaMaskPopup() + .waitForElementPresent('[data-testid="page-container-footer-next"]') + .click('[data-testid="page-container-footer-next"]') // approve the tx + .switchBrowserTab(0) // back to remix + .waitForElementContainsText('*[data-id="terminalJournal"]', 'view on etherscan', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', 'from: 0x76a...2708f', 60000) + .perform(() => done()) + }) + }) + }, + + // main network tests + 'Should connect to Ethereum Main Network using MetaMask #group1': function (browser: NightwatchBrowser) { + browser.waitForElementPresent('*[data-id="remixIdeSidePanel"]') + .switchBrowserTab(1) + .click('[data-testid="network-display"]') + .click('div[data-testid="Ethereum Mainnet"]') // switch to mainnet + .useCss().switchBrowserTab(0) + .refreshPage() + .waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) + .click('*[data-id="landingPageStartSolidity"]') + .clickLaunchIcon('udapp') + .switchEnvironment('injected-MetaMask') + .waitForElementPresent('*[data-id="settingsNetworkEnv"]') + .assert.containsText('*[data-id="settingsNetworkEnv"]', 'Main (1) network') + }, + + 'Should deploy contract on Ethereum Main Network using MetaMask #group1': function (browser: NightwatchBrowser) { + browser.waitForElementPresent('*[data-id="runTabSelectAccount"] option') + .clickLaunchIcon('filePanel') + .addFile('Greet.sol', sources[0]['Greet.sol']) + .clickLaunchIcon('udapp') + .waitForElementPresent('*[data-id="Deploy - transact (not payable)"]') + .click('*[data-id="Deploy - transact (not payable)"]') + .waitForElementVisible('*[data-id="udappNotifyModalDialogModalBody-react"]', 65000) + .modalFooterOKClick('udappNotify') + .pause(10000) + .assert.containsText('*[data-id="udappNotifyModalDialogModalBody-react"]', 'You are about to create a transaction on Main Network. Confirm the details to send the info to your provider.') + .modalFooterCancelClick('udappNotify') + }, + // debug transaction + 'Should deploy Ballot to Sepolia using metamask #group1 #flaky': function (browser: NightwatchBrowser) { + browser.waitForElementPresent('*[data-id="remixIdeSidePanel"]') + .switchBrowserTab(1) + .click('[data-testid="network-display"]') + .click('div[data-testid="Sepolia"]') // switch to sepolia + .useCss().switchBrowserTab(0) + .addFile('BallotTest.sol', examples.ballot) + .clickLaunchIcon('udapp') + .clearConsole() + .clearTransactions() + .clickLaunchIcon('udapp') + .waitForElementVisible('input[placeholder="bytes32[] proposalNames"]') + .pause(2000) + .setValue('input[placeholder="bytes32[] proposalNames"]', '["0x48656c6c6f20576f726c64210000000000000000000000000000000000000000"]') + .pause(1000) + .click('*[data-id="Deploy - transact (not payable)"]') // deploy ballot + .pause(1000) + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: "//span[@class='text-log' and contains(., 'creation of')]" + }) + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: "//span[@class='text-log' and contains(., 'pending')]" + }) + .perform((done) => { + browser.switchBrowserWindow(extension_url, 'MetaMask', (browser) => { + browser + .maximizeWindow() + .hideMetaMaskPopup() + .pause(3000) + .waitForElementPresent('[data-testid="page-container-footer-next"]') + .scrollAndClick('[data-testid="page-container-footer-next"]') + .pause(2000) + .switchBrowserTab(0) // back to remix + .waitForElementContainsText('*[data-id="terminalJournal"]', 'view on etherscan', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', 'from: 0x76a...2708f', 60000) + .perform(() => done()) + }) + }) + }, + + 'do transaction #group1': function (browser: NightwatchBrowser) { + browser.waitForElementPresent('*[data-id="universalDappUiContractActionWrapper"]', 60000) + .clearConsole() + .clickInstance(0) + .clickFunction('delegate - transact (not payable)', { types: 'address to', values: '"0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db"' }) + .pause(5000) + .perform((done) => { // call delegate + browser.switchBrowserWindow(extension_url, 'MetaMask', (browser) => { + browser + .maximizeWindow() + .hideMetaMaskPopup() + .pause(5000) + .waitForElementPresent('[data-testid="page-container-footer-next"]') + .scrollAndClick('[data-testid="page-container-footer-next"]') + .pause(2000) + .switchBrowserTab(0) // back to remix + .waitForElementContainsText('*[data-id="terminalJournal"]', 'view on etherscan', 60000) + .waitForElementContainsText('*[data-id="terminalJournal"]', 'from: 0x76a...2708f', 60000) + .perform(() => done()) + }) + }) + .testFunction('last', + { + status: '0x1 Transaction mined and execution succeed', + 'decoded input': { 'address to': '0x4B0897b0513fdC7C541B6d9D7E929C4e5364D2dB' } + }) + }, + 'Should debug Sepolia transaction with source highlighting MetaMask #group1': function (browser: NightwatchBrowser) { + let txhash + browser.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) + .clickLaunchIcon('pluginManager') // load debugger and source verification + .clickLaunchIcon('udapp') + .perform((done) => { + browser.getLastTransactionHash((hash) => { + txhash = hash + done() + }) + }) + .pause(5000) + .perform((done) => { + browser + .waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) + .clickLaunchIcon('debugger') + .waitForElementVisible('*[data-id="debuggerTransactionInput"]') + .setValue('*[data-id="debuggerTransactionInput"]', txhash) // debug tx + .pause(2000) + .click('*[data-id="debuggerTransactionStartButton"]') + .waitForElementVisible('*[data-id="treeViewDivto"]', 30000) + .checkVariableDebug('soliditylocals', localsCheck) + .perform(() => done()) + }) + + }, + 'Call web3.eth.getAccounts() using Injected Provider (Metamask) #group1': function (browser: NightwatchBrowser) { + if (!checkBrowserIsChrome(browser)) return + browser + .executeScriptInTerminal('web3.eth.getAccounts()') + .journalLastChildIncludes('["0x76a3ABb5a12dcd603B52Ed22195dED17ee82708f"]') + }, + // EIP 712 tests + 'Test EIP 712 Signature with Injected Provider (Metamask) #group1': function (browser: NightwatchBrowser) { + browser + .clickLaunchIcon('udapp') + .waitForElementPresent('i[id="remixRunSignMsg"]') + .click('i[id="remixRunSignMsg"]') + .waitForElementVisible('*[data-id="signMessageTextarea"]', 120000) + .click('*[data-id="sign-eip-712"]') + .waitForElementVisible('*[data-id="udappNotify-modal-footer-ok-react"]') + .modalFooterOKClick('udappNotify') + .pause(1000) + .getEditorValue((content) => { + browser.assert.ok(content.indexOf('"primaryType": "AuthRequest",') !== -1, 'EIP 712 data file must be opened') + }) + .setEditorValue(JSON.stringify(EIP712_Example, null, '\t')) + .pause(5000) + .clickLaunchIcon('filePanel') + .rightClick('li[data-id="treeViewLitreeViewItemEIP-712-data.json"]') + .click('*[data-id="contextMenuItemsignTypedData"]') + .pause(1000) + .perform((done) => { // call delegate + browser.switchBrowserWindow(extension_url, 'MetaMask', (browser) => { + browser + .maximizeWindow() + .hideMetaMaskPopup() + .pause(1000) + .waitForElementPresent('[data-testid="page-container-footer-next"]') + .scrollAndClick('button[data-testid="page-container-footer-next"]') // confirm + .switchBrowserTab(0) // back to remix + .perform(() => done()) + }) + }) + .pause(1000) + .journalChildIncludes('0xec72bbabeb47a3a766af449674a45a91a6e94e35ebf0ae3c644b66def7bd387f1c0b34d970c9f4a1e9398535e5860b35e82b2a8931b7c9046b7766a53e66db3d1b') + } +} + +const branch = process.env.CIRCLE_BRANCH +const runTestsConditions = branch && (branch === 'master' || branch === 'remix_live' || branch.includes('remix_beta') || branch.includes('metamask')) + +if (!checkBrowserIsChrome(browser)) { + module.exports = {} +} else { + module.exports = { + ...(branch ? (runTestsConditions ? tests : {}) : tests) + }; +} + + +const EIP712_Example = { + domain: { + chainId: 11155111, + name: "Example App", + verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + version: "1", + }, + message: { + prompt: "Welcome! In order to authenticate to this website, sign this request and your public address will be sent to the server in a verifiable way.", + createdAt: 1718570375196, + }, + primaryType: 'AuthRequest', + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + AuthRequest: [ + { name: 'prompt', type: 'string' }, + { name: 'createdAt', type: 'uint256' }, + ], + }, +} + + +const sources = [ + { + 'Greet.sol': { + content: + ` + pragma solidity ^0.8.0; + contract HelloWorld { + string public message; + + fallback () external { + message = 'Hello World!'; + } + + function greet(string memory _message) public { + message = _message; + } + }` + }, + 'faulty.sol': { + content: `// SPDX-License-Identifier: GPL-3.0 + + pragma solidity >=0.8.2 <0.9.0; + + contract Test { + error O_o(uint256); + constructor() { + revert O_o(block.timestamp); + } + }` + } + } +] diff --git a/apps/remix-ide-e2e/src/tests/providers.test.ts b/apps/remix-ide-e2e/src/tests/providers.test.ts index 7fb52593e2..862fed757b 100644 --- a/apps/remix-ide-e2e/src/tests/providers.test.ts +++ b/apps/remix-ide-e2e/src/tests/providers.test.ts @@ -3,54 +3,119 @@ import { NightwatchBrowser } from 'nightwatch' import init from '../helpers/init' module.exports = { + '@disabled': true, before: function (browser: NightwatchBrowser, done: VoidFunction) { init(browser, done, 'http://127.0.0.1:8080', false) }, - 'Should switch to ganache provider, set a custom URL and fail to connect': function (browser: NightwatchBrowser) { + 'Should switch to ganache provider, set a custom URL and fail to connect #group1': function (browser: NightwatchBrowser) { browser.waitForElementVisible('div[data-id="remixIdeIconPanel"]', 10000) .clickLaunchIcon('udapp') .switchEnvironment('ganache-provider') .waitForElementVisible('*[data-id="ganache-providerModalDialogModalBody-react"]') + .execute(() => { (document.querySelector('*[data-id="ganache-providerModalDialogModalBody-react"] input') as any).focus() - }, [], () => {}) + }, [], () => { }) .clearValue('*[data-id="ganache-providerModalDialogModalBody-react"] input') .setValue('*[data-id="ganache-providerModalDialogModalBody-react"] input', 'http://127.0.0.1:8084') .modalFooterOKClick('ganache-provider') - .waitForElementContainsText('*[data-id="ganache-providerModalDialogModalBody-react"]', 'Error while connecting to the provider') - .modalFooterOKClick('ganache-provider') - .waitForElementNotVisible('*[data-id="ganache-providerModalDialogModalBody-react"]') + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: "//span[@class='text-danger' and contains(., 'missing response')]" + }) + .waitForElementPresent({ selector: `[data-id="selected-provider-ganache-provider"]`, timeout: 5000 }) .pause(1000) }, - 'Should switch to ganache provider, use the default ganache URL and succeed to connect': function (browser: NightwatchBrowser) { - browser.switchEnvironment('ganache-provider') - .waitForElementVisible('*[data-id="ganache-providerModalDialogModalBody-react"]') - .modalFooterOKClick('ganache-provider') - .waitForElementContainsText('*[data-id="settingsNetworkEnv"]', 'Custom (') + 'Should switch to ganache provider, use the default ganache URL and succeed to connect #group1': function (browser: NightwatchBrowser) { + browser + .switchEnvironment('vm-cancun') + .pause(2000) + .switchEnvironment('ganache-provider') + .waitForElementVisible('*[data-id="ganache-providerModalDialogModalBody-react"]') + .modalFooterOKClick('ganache-provider') + .waitForElementContainsText('*[data-id="settingsNetworkEnv"]', 'Custom (') + .waitForElementVisible({ selector: `[data-id="selected-provider-ganache-provider"]`, timeout: 5000 }) + }, - 'Should switch to foundry provider, set a custom URL and fail to connect': function (browser: NightwatchBrowser) { + 'Should switch to foundry provider, set a custom URL and fail to connect #group1': function (browser: NightwatchBrowser) { browser.waitForElementVisible('div[data-id="remixIdeIconPanel"]', 10000) - .switchEnvironment('foundry-provider') - .waitForElementVisible('*[data-id="foundry-providerModalDialogModalBody-react"]') - .execute(() => { - (document.querySelector('*[data-id="foundry-providerModalDialogModalBody-react"] input') as any).focus() - }, [], () => {}) - .clearValue('*[data-id="foundry-providerModalDialogModalBody-react"] input') - .setValue('*[data-id="foundry-providerModalDialogModalBody-react"] input', 'http://127.0.0.1:8084') - .modalFooterOKClick('foundry-provider') - .waitForElementContainsText('*[data-id="foundry-providerModalDialogModalBody-react"]', 'Error while connecting to the provider') - .modalFooterOKClick('foundry-provider') - .waitForElementNotVisible('*[data-id="foundry-providerModalDialogModalBody-react"]') - .pause(1000) - -}, - 'Should switch to foundry provider, use the default foundry URL and succeed to connect': function (browser: NightwatchBrowser) { + .switchEnvironment('foundry-provider') + .waitForElementVisible('*[data-id="foundry-providerModalDialogModalBody-react"]') + .execute(() => { + (document.querySelector('*[data-id="foundry-providerModalDialogModalBody-react"] input') as any).focus() + }, [], () => { }) + .clearValue('*[data-id="foundry-providerModalDialogModalBody-react"] input') + .setValue('*[data-id="foundry-providerModalDialogModalBody-react"] input', 'http://127.0.0.1:8084') + .modalFooterOKClick('foundry-provider') + .pause(1000) + + }, + 'Should switch to foundry provider, use the default foundry URL and succeed to connect #group1': !function (browser: NightwatchBrowser) { browser.switchEnvironment('foundry-provider') - .waitForElementVisible('*[data-id="foundry-providerModalDialogModalBody-react"]') - .modalFooterOKClick('foundry-provider') - .waitForElementContainsText('*[data-id="settingsNetworkEnv"]', 'Custom (') + .waitForElementVisible('*[data-id="foundry-providerModalDialogModalBody-react"]') + .modalFooterOKClick('foundry-provider') + .waitForElementContainsText('*[data-id="settingsNetworkEnv"]', 'Custom (') + }, + + 'Should switch to custom provider #group2': function (browser: NightwatchBrowser) { + browser.waitForElementVisible('div[data-id="remixIdeIconPanel"]', 10000) + .clickLaunchIcon('udapp') + .switchEnvironment('ganache-provider') + .waitForElementVisible('*[data-id="ganache-providerModalDialogModalBody-react"]') + + .execute(() => { + (document.querySelector('*[data-id="ganache-providerModalDialogModalBody-react"] input') as any).focus() + }, [], () => { }) + .clearValue('*[data-id="ganache-providerModalDialogModalBody-react"] input') + .setValue('*[data-id="ganache-providerModalDialogModalBody-react"] input', 'https://scroll-rpc.publicnode.com') + .modalFooterOKClick('ganache-provider') + .pause(100) + .waitForElementPresent({ selector: `[data-id="selected-provider-ganache-provider"]`, timeout: 5000 }) + .pause(1000) + }, + + 'execute script #group2': function (browser: NightwatchBrowser) { + browser.clickLaunchIcon('filePanel') + .addFile('testScript.ts', { content: testScript }) + .clearConsole() + .pause(10000) + .waitForElementVisible('*[data-id="play-editor"]') + .click('*[data-id="play-editor"]') + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: "//span[@class='text-danger' and contains(., 'exceed maximum block range')]" + }) + .waitForElementPresent({ selector: `[data-id="selected-provider-ganache-provider"]`, timeout: 5000 }) } } + + +const testScript = ` +// Importing necessary libraries from Ethers.js for interaction with Ethereum blockchain. +import { ethers } from "hardhat"; + +// https://scroll-rpc.publicnode.com +async function main() { + // Setting up provider (RPC URL) to interact with your chosen Ethereum chain, + const [deployer] = await ethers.getSigners(); + + try{ + let provider; + if(!provider){ + provider=ethers.provider; + } + + const contractAddress = "0x2bC16Bf30435fd9B3A3E73Eb759176C77c28308D"; // Replace with your smart contract's address. + + // Retrieving all events of a specific kind from the blockchain + let logs = await provider.getLogs({address:contractAddress, fromBlock: '0x332f23',toBlock: '0x384410', topics: ['0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef']}); + console.log("Got Logs ",logs) + }catch(error){ + } + +} + +main()` \ No newline at end of file diff --git a/apps/remix-ide-e2e/src/tests/script-runner.test.ts b/apps/remix-ide-e2e/src/tests/script-runner.test.ts new file mode 100644 index 0000000000..493466983d --- /dev/null +++ b/apps/remix-ide-e2e/src/tests/script-runner.test.ts @@ -0,0 +1,116 @@ +'use strict' +import { NightwatchBrowser } from 'nightwatch' +import init from '../helpers/init' + +module.exports = { + + before: function (browser: NightwatchBrowser, done: VoidFunction) { + init(browser, done, 'http://127.0.0.1:8080', false) + }, + 'Should load default script runner': function (browser: NightwatchBrowser) { + browser + .clickLaunchIcon('scriptRunnerBridge') + .waitForElementVisible('[data-id="sr-loaded-default"]') + .waitForElementVisible('[data-id="dependency-ethers-^5"]') + .waitForElementVisible('[data-id="sr-toggle-ethers6"]') + }, + 'Should load script runner ethers6': function (browser: NightwatchBrowser) { + browser + .click('[data-id="sr-toggle-ethers6"]') + .waitForElementVisible('[data-id="sr-loaded-ethers6"]') + .waitForElementPresent('[data-id="dependency-ethers-^6"]') + }, + 'Should have config file in .remix/script.config.json': function (browser: NightwatchBrowser) { + browser + .clickLaunchIcon('filePanel') + .waitForElementVisible('[data-path=".remix"]') + .waitForElementVisible('[data-id="treeViewDivDraggableItem.remix/script.config.json"]') + .openFile('.remix/script.config.json') + }, + 'check config file content': function (browser: NightwatchBrowser) { + browser + .getEditorValue((content) => { + console.log(JSON.parse(content)) + const parsed = JSON.parse(content) + browser.assert.ok(parsed.defaultConfig === 'ethers6', 'config file content is correct') + }) + }, + 'execute ethers6 script': function (browser: NightwatchBrowser) { + browser + .click('*[data-id="treeViewUltreeViewMenu"]') // make sure we create the file at the root folder + .addFile('deployWithEthersJs.js', { content: deployWithEthersJs }) + .pause(1000) + .click('[data-id="treeViewDivtreeViewItemcontracts"]') + .openFile('contracts/2_Owner.sol') + .clickLaunchIcon('solidity') + .click('*[data-id="compilerContainerCompileBtn"]') + .executeScriptInTerminal('remix.execute(\'deployWithEthersJs.js\')') + .waitForElementContainsText('*[data-id="terminalJournal"]', '0xd9145CCE52D386f254917e481eB44e9943F39138', 60000) + }, + 'switch workspace it should be default again': function (browser: NightwatchBrowser) { + browser + .clickLaunchIcon('filePanel') + .pause(2000) + .waitForElementVisible('*[data-id="workspacesMenuDropdown"]') + .click('*[data-id="workspacesMenuDropdown"]') + .click('*[data-id="workspacecreate"]') + .waitForElementPresent('*[data-id="create-semaphore"]') + .scrollAndClick('*[data-id="create-semaphore"]') + .modalFooterOKClick('TemplatesSelection') + .clickLaunchIcon('scriptRunnerBridge') + .waitForElementVisible('[data-id="sr-loaded-default"]') + .waitForElementVisible('[data-id="dependency-ethers-^5"]') + .waitForElementVisible('[data-id="sr-toggle-ethers6"]') + }, + 'switch to default workspace that should be on ethers6': function (browser: NightwatchBrowser) { + browser + .clickLaunchIcon('filePanel') + .switchWorkspace('default_workspace') + .clickLaunchIcon('scriptRunnerBridge') + .waitForElementVisible('[data-id="sr-loaded-ethers6"]') + .waitForElementPresent('[data-id="dependency-ethers-^6"]') + } +} + + +const deployWithEthersJs = ` +import { ethers } from 'ethers' + +/** + * Deploy the given contract + * @param {string} contractName name of the contract to deploy + * @param {Array} args list of constructor' parameters + * @param {Number} accountIndex account index from the exposed account + * @return {Contract} deployed contract + * + */ +const deploy = async (contractName: string, args: Array, accountIndex?: number): Promise => { + + console.log(\`deploying \${contractName}\`) + // Note that the script needs the ABI which is generated from the compilation artifact. + // Make sure contract is compiled and artifacts are generated + const artifactsPath = \`contracts/artifacts/\${contractName}.json\` // Change this for different path + + const metadata = JSON.parse(await remix.call('fileManager', 'getFile', artifactsPath)) + // 'web3Provider' is a remix global variable object + + const signer = await (new ethers.BrowserProvider(web3Provider)).getSigner(accountIndex) + + const factory = new ethers.ContractFactory(metadata.abi, metadata.data.bytecode.object, signer) + + const contract = await factory.deploy(...args) + + // The contract is NOT deployed yet; we must wait until it is mined + await contract.waitForDeployment() + return contract +} + +(async () => { + try { + const contract = await deploy('Owner', []) + + console.log(\`address: \${await contract.getAddress()}\`) + } catch (e) { + console.log(e.message) + } +})()` \ No newline at end of file diff --git a/apps/remix-ide/ci/browser_test.sh b/apps/remix-ide/ci/browser_test.sh index 8af369b567..ed1e968109 100755 --- a/apps/remix-ide/ci/browser_test.sh +++ b/apps/remix-ide/ci/browser_test.sh @@ -15,7 +15,7 @@ sleep 5 # grep -IRiL "@disabled" "dist/apps/remix-ide-e2e/src/tests" | grep "\.spec\|\.test" | xargs -I {} basename {} .test.js | grep -E "\b[${2}]" # TESTFILES=$(grep -IRiL "@disabled" "dist/apps/remix-ide-e2e/src/tests" | grep "\.spec\|\.test" | xargs -I {} basename {} .test.js | grep -E "\b[$2]" | circleci tests split --split-by=timings ) node apps/remix-ide/ci/splice_tests.js $2 $3 -TESTFILES=$(node apps/remix-ide/ci/splice_tests.js $2 $3 | circleci tests split --split-by=timings) +TESTFILES=$(node apps/remix-ide/ci/splice_tests.js $2 $3 | grep -v 'metamask' | circleci tests split --split-by=timings) for TESTFILE in $TESTFILES; do npx nightwatch --config dist/apps/remix-ide-e2e/nightwatch-${1}.js dist/apps/remix-ide-e2e/src/tests/${TESTFILE}.js --env=$1 || npx nightwatch --config dist/apps/remix-ide-e2e/nightwatch-${1}.js dist/apps/remix-ide-e2e/src/tests/${TESTFILE}.js --env=$1 || TEST_EXITCODE=1 done diff --git a/apps/remix-ide/ci/metamask.sh b/apps/remix-ide/ci/metamask.sh new file mode 100755 index 0000000000..8e29c876f1 --- /dev/null +++ b/apps/remix-ide/ci/metamask.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +set -e + +TESTFILES=$(grep -IRiL "\'@disabled\': \?true" "dist/apps/remix-ide-e2e/src/tests" | grep "metamask" | sort ) + +# count test files +fileCount=$(grep -IRiL "\'@disabled\': \?true" "dist/apps/remix-ide-e2e/src/tests" | grep "metamask" | wc -l ) +# if fileCount is 0 +if [ $fileCount -eq 0 ] +then + echo "No metamask tests found" + exit 0 +fi + +BUILD_ID=${CIRCLE_BUILD_NUM:-${TRAVIS_JOB_NUMBER}} +echo "$BUILD_ID" +TEST_EXITCODE=0 + +npx ganache & +npx http-server -p 9090 --cors='*' ./node_modules & +yarn run serve:production & +sleep 5 + +for TESTFILE in $TESTFILES; do + npx nightwatch --config dist/apps/remix-ide-e2e/nightwatch-${1}.js $TESTFILE --env=$1 || TEST_EXITCODE=1 +done + +echo "$TEST_EXITCODE" +if [ "$TEST_EXITCODE" -eq 1 ] +then + exit 1 +fi diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index d9a4cf6f13..23addcdf90 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -78,6 +78,7 @@ const remixLib = require('@remix-project/remix-lib') import { QueryParams } from '@remix-project/remix-lib' import { SearchPlugin } from './app/tabs/search' +import { ScriptRunnerUIPlugin } from './app/tabs/script-runner-ui' import { ElectronProvider } from './app/files/electronProvider' const Storage = remixLib.Storage @@ -247,6 +248,9 @@ class AppComponent { //----- search const search = new SearchPlugin() + //---------------- Script Runner UI Plugin ------------------------- + const scriptRunnerUI = new ScriptRunnerUIPlugin(this.engine) + //---- templates const templates = new TemplatesPlugin() @@ -397,6 +401,7 @@ class AppComponent { pluginStateLogger, matomo, templateSelection, + scriptRunnerUI, remixAI ]) @@ -652,7 +657,7 @@ class AppComponent { }) // activate solidity plugin - this.appManager.activatePlugin(['solidity', 'udapp', 'deploy-libraries', 'link-libraries', 'openzeppelin-proxy']) + this.appManager.activatePlugin(['solidity', 'udapp', 'deploy-libraries', 'link-libraries', 'openzeppelin-proxy', 'scriptRunnerBridge']) } } diff --git a/apps/remix-ide/src/app/components/hidden-panel.tsx b/apps/remix-ide/src/app/components/hidden-panel.tsx index 27993313c0..0629d2014f 100644 --- a/apps/remix-ide/src/app/components/hidden-panel.tsx +++ b/apps/remix-ide/src/app/components/hidden-panel.tsx @@ -24,10 +24,16 @@ export class HiddenPanel extends AbstractPanel { addView(profile: any, view: any): void { super.removeView(profile) + this.renderComponent() super.addView(profile, view) this.renderComponent() } + removeView(profile: any): void { + super.removeView(profile) + this.renderComponent() + } + updateComponent(state: any) { return } plugins={state.plugins} /> } diff --git a/apps/remix-ide/src/app/panels/terminal.tsx b/apps/remix-ide/src/app/panels/terminal.tsx index 2cb89e3fcb..a30cf085bd 100644 --- a/apps/remix-ide/src/app/panels/terminal.tsx +++ b/apps/remix-ide/src/app/panels/terminal.tsx @@ -117,10 +117,10 @@ class Terminal extends Plugin { } onDeactivation() { - this.off('scriptRunner', 'log') - this.off('scriptRunner', 'info') - this.off('scriptRunner', 'warn') - this.off('scriptRunner', 'error') + this.off('scriptRunnerBridge', 'log') + this.off('scriptRunnerBridge', 'info') + this.off('scriptRunnerBridge', 'warn') + this.off('scriptRunnerBridge', 'error') } logHtml(html) { diff --git a/apps/remix-ide/src/app/plugins/matomo.ts b/apps/remix-ide/src/app/plugins/matomo.ts index e6af25655c..0421ec6a49 100644 --- a/apps/remix-ide/src/app/plugins/matomo.ts +++ b/apps/remix-ide/src/app/plugins/matomo.ts @@ -11,7 +11,7 @@ const profile = { version: '1.0.0' } -const allowedPlugins = ['LearnEth', 'etherscan', 'vyper', 'circuit-compiler', 'doc-gen', 'doc-viewer', 'solhint', 'walletconnect', 'scriptRunner', 'dgit', 'contract-verification'] +const allowedPlugins = ['LearnEth', 'etherscan', 'vyper', 'circuit-compiler', 'doc-gen', 'doc-viewer', 'solhint', 'walletconnect', 'scriptRunner', 'scriptRunnerBridge', 'dgit', 'contract-verification'] export class Matomo extends Plugin { diff --git a/apps/remix-ide/src/app/plugins/parser/services/code-parser-compiler.ts b/apps/remix-ide/src/app/plugins/parser/services/code-parser-compiler.ts index 073347a33b..d29d427aea 100644 --- a/apps/remix-ide/src/app/plugins/parser/services/code-parser-compiler.ts +++ b/apps/remix-ide/src/app/plugins/parser/services/code-parser-compiler.ts @@ -161,7 +161,7 @@ export default class CodeParserCompiler { "*": ["evm.gasEstimates"] } }, - "evmVersion": state.evmVersion && state.evmVersion.toString() || "cancun", + "evmVersion": state.evmVersion && state.evmVersion.toString() || undefined, } } @@ -266,4 +266,4 @@ export default class CodeParserCompiler { return result } -} \ No newline at end of file +} diff --git a/apps/remix-ide/src/app/providers/abstract-provider.tsx b/apps/remix-ide/src/app/providers/abstract-provider.tsx index b00efc3d0a..6c0d866265 100644 --- a/apps/remix-ide/src/app/providers/abstract-provider.tsx +++ b/apps/remix-ide/src/app/providers/abstract-provider.tsx @@ -26,8 +26,8 @@ export type RejectRequest = (error: JsonDataResult) => void export type SuccessRequest = (data: JsonDataResult) => void export interface IProvider { - options: {[id: string]: any} - init(): Promise<{[id: string]: any}> + options: { [id: string]: any } + init(): Promise<{ [id: string]: any }> body(): JSX.Element sendAsync(data: JsonDataRequest): Promise } @@ -38,7 +38,7 @@ export abstract class AbstractProvider extends Plugin implements IProvider { defaultUrl: string connected: boolean nodeUrl: string - options: {[id: string]: any} = {} + options: { [id: string]: any } = {} constructor(profile, blockchain, defaultUrl) { super(profile) @@ -102,24 +102,16 @@ export abstract class AbstractProvider extends Plugin implements IProvider { sendAsync(data: JsonDataRequest): Promise { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { - if (!this.provider) return reject({ jsonrpc: '2.0', id: data.id, error: { message: 'provider node set', code: -32603 } } as JsonDataResult) + if (!this.provider) return reject({ jsonrpc: '2.0', id: data.id, error: { message: 'provider not set', code: -32603 } } as JsonDataResult) this.sendAsyncInternal(data, resolve, reject) }) } - private async switchAway(showError) { + private async switchAway(showError: boolean, msg: string) { if (!this.provider) return - this.provider = null - this.connected = false if (showError) { - const modalContent: AlertModal = { - id: this.profile.name, - title: this.profile.displayName, - message: `Error while connecting to the provider, provider not connected` - } - this.call('notification', 'alert', modalContent) + this.call('terminal', 'log', { type: 'error', value: 'Error while querying the provider: ' + msg }) } - await this.call('udapp', 'setEnvironmentMode', { context: 'vm-cancun' }) return } @@ -130,10 +122,22 @@ export abstract class AbstractProvider extends Plugin implements IProvider { resolve({ jsonrpc: '2.0', result, id: data.id }) } catch (error) { if (error && error.message && error.message.includes('SERVER_ERROR')) { - this.switchAway(true) + try { + // replace escaped quotes with normal quotes + const errorString = String(error.message).replace(/\\"/g, '"'); + const messageMatches = Array.from(errorString.matchAll(/"message":"(.*?)"/g)); + // Extract the message values + const messages = messageMatches.map(match => match[1]); + if (messages && messages.length > 0) { + this.switchAway(true, messages[0]) + } else { + this.switchAway(true, error.message ? error.message : error.error ? error.error : error) + } + } catch (error) { + this.switchAway(true, error.message ? error.message : error.error ? error.error : error) + } } - error.code = -32603 - reject({ jsonrpc: '2.0', error, id: data.id }) + reject({ jsonrpc: '2.0', error: { message: error.message, code: -32603 }, id: data.id }) } } else { const result = data.method === 'net_listening' ? 'canceled' : [] diff --git a/apps/remix-ide/src/app/tabs/compile-and-run.ts b/apps/remix-ide/src/app/tabs/compile-and-run.ts index 63952747c9..9643cd78e9 100644 --- a/apps/remix-ide/src/app/tabs/compile-and-run.ts +++ b/apps/remix-ide/src/app/tabs/compile-and-run.ts @@ -62,7 +62,7 @@ export class CompileAndRun extends Plugin { if (clearAllInstances) { await this.call('udapp', 'clearAllInstances') } - await this.call('scriptRunner', 'execute', content, fileName) + await this.call('scriptRunnerBridge', 'execute', content, fileName) } catch (e) { this.call('notification', 'toast', e.message || e) } diff --git a/apps/remix-ide/src/app/tabs/locales/en/remixUiTabs.json b/apps/remix-ide/src/app/tabs/locales/en/remixUiTabs.json index b3f77de301..5870ea16e0 100644 --- a/apps/remix-ide/src/app/tabs/locales/en/remixUiTabs.json +++ b/apps/remix-ide/src/app/tabs/locales/en/remixUiTabs.json @@ -7,6 +7,7 @@ "remixUiTabs.tooltipText6": "Enable RemixAI Copilot [BETA]", "remixUiTabs.tooltipText7": "Disable RemixAI Copilot [BETA]", "remixUiTabs.tooltipText8": "Remix AI Tools Documentation", + "remixUiTabs.tooltipText9": "Configure scripting dependencies", "remixUiTabs.tooltipTextDisabledCopilot": "To use RemixAI Copilot, choose a .sol file", "remixUiTabs.zoomOut": "Zoom out", "remixUiTabs.zoomIn": "Zoom in" diff --git a/apps/remix-ide/src/app/tabs/script-runner-ui.tsx b/apps/remix-ide/src/app/tabs/script-runner-ui.tsx new file mode 100644 index 0000000000..7e108c71ca --- /dev/null +++ b/apps/remix-ide/src/app/tabs/script-runner-ui.tsx @@ -0,0 +1,399 @@ +import { IframePlugin, IframeProfile, ViewPlugin } from '@remixproject/engine-web' +import * as packageJson from '../../../../../package.json' +import React from 'react' // eslint-disable-line +import { customScriptRunnerConfig, ProjectConfiguration, ScriptRunnerConfig, ScriptRunnerUI } from '@remix-scriptrunner' // eslint-disable-line +import { Profile } from '@remixproject/plugin-utils' +import { Engine, Plugin } from '@remixproject/engine' +import axios from 'axios' +import { AppModal } from '@remix-ui/app' +import { isArray } from 'lodash' +import { PluginViewWrapper } from '@remix-ui/helper' +import { CustomRemixApi } from '@remix-api' + +const profile = { + name: 'scriptRunnerBridge', + displayName: 'Script configuration', + methods: ['execute'], + events: ['log', 'info', 'warn', 'error'], + icon: 'assets/img/solid-gear-circle-play.svg', + description: 'Configure the dependencies for running scripts.', + kind: '', + location: 'sidePanel', + version: packageJson.version, + maintainedBy: 'Remix' +} + +const configFileName = '.remix/script.config.json' + +let baseUrl = 'https://remix-project-org.github.io/script-runner-generator' +const customBuildUrl = 'http://localhost:4000/build' // this will be used when the server is ready + +interface IScriptRunnerState { + customConfig: customScriptRunnerConfig + configurations: ProjectConfiguration[] + activeConfig: ProjectConfiguration + enableCustomScriptRunner: boolean +} + +export class ScriptRunnerUIPlugin extends ViewPlugin { + engine: Engine + dispatch: React.Dispatch = () => { } + workspaceScriptRunnerDefaults: Record + customConfig: ScriptRunnerConfig + configurations: ProjectConfiguration[] + activeConfig: ProjectConfiguration + enableCustomScriptRunner: boolean + plugin: Plugin + scriptRunnerProfileName: string + constructor(engine: Engine) { + super(profile) + this.engine = engine + this.workspaceScriptRunnerDefaults = {} + this.plugin = this + this.enableCustomScriptRunner = false // implement this later + } + + async onActivation() { + + this.on('filePanel', 'setWorkspace', async (workspace: string) => { + this.activeConfig = null + this.customConfig = + { + defaultConfig: 'default', + customConfig: { + baseConfiguration: 'default', + dependencies: [] + } + } + await this.loadCustomConfig() + await this.loadConfigurations() + this.renderComponent() + }) + + this.plugin.on('fileManager', 'fileSaved', async (file: string) => { + + if (file === configFileName && this.enableCustomScriptRunner) { + await this.loadCustomConfig() + this.renderComponent() + } + }) + await this.loadCustomConfig() + await this.loadConfigurations() + this.renderComponent() + } + + render() { + return ( +
+ +
+ ) + } + + setDispatch(dispatch: React.Dispatch) { + this.dispatch = dispatch + this.renderComponent() + } + + renderComponent() { + this.dispatch({ + customConfig: this.customConfig, + configurations: this.configurations, + activeConfig: this.activeConfig, + enableCustomScriptRunner: this.enableCustomScriptRunner + }) + } + + updateComponent(state: IScriptRunnerState) { + return ( + + ) + } + + async selectScriptRunner(config: ProjectConfiguration) { + if (await this.loadScriptRunner(config)) + await this.saveCustomConfig(this.customConfig) + } + + async loadScriptRunner(config: ProjectConfiguration): Promise { + + const profile: Profile = await this.plugin.call('manager', 'getProfile', 'scriptRunner') + this.scriptRunnerProfileName = profile.name + const testPluginName = localStorage.getItem('test-plugin-name') + const testPluginUrl = localStorage.getItem('test-plugin-url') + + let url = `${baseUrl}?template=${config.name}×tamp=${Date.now()}` + if (testPluginName === 'scriptRunner') { + // if testpluginurl has template specified only use that + if (testPluginUrl.indexOf('template') > -1) { + url = testPluginUrl + } else { + baseUrl = `//${new URL(testPluginUrl).host}` + url = `${baseUrl}?template=${config.name}×tamp=${Date.now()}` + } + } + //console.log('loadScriptRunner', profile) + const newProfile: IframeProfile = { + ...profile, + name: profile.name + config.name, + location: 'hiddenPanel', + url: url + } + + let result = null + try { + this.setIsLoading(config.name, true) + const plugin: IframePlugin = new IframePlugin(newProfile) + if (!this.engine.isRegistered(newProfile.name)) { + + await this.engine.register(plugin) + } + await this.plugin.call('manager', 'activatePlugin', newProfile.name) + + this.activeConfig = config + this.on(newProfile.name, 'log', this.log.bind(this)) + this.on(newProfile.name, 'info', this.info.bind(this)) + this.on(newProfile.name, 'warn', this.warn.bind(this)) + this.on(newProfile.name, 'error', this.error.bind(this)) + this.on(newProfile.name, 'dependencyError', this.dependencyError.bind(this)) + this.customConfig.defaultConfig = config.name + this.setErrorStatus(config.name, false, '') + result = true + } catch (e) { + console.log('Error loading script runner: ', newProfile.name, e) + const iframe = document.getElementById(`plugin-${newProfile.name}`); + if (iframe) { + await this.call('hiddenPanel', 'removeView', newProfile) + } + + delete (this.engine as any).manager.profiles[newProfile.name] + delete (this.engine as any).plugins[newProfile.name] + console.log('Error loading script runner: ', newProfile.name, e) + this.setErrorStatus(config.name, true, e) + result = false + } + + this.setIsLoading(config.name, false) + this.renderComponent() + return result + + } + + async execute(script: string, filePath: string) { + this.call('terminal', 'log', { value: `running ${filePath} ...`, type: 'info' }) + if (!this.scriptRunnerProfileName || !this.engine.isRegistered(`${this.scriptRunnerProfileName}${this.activeConfig.name}`)) { + if (!await this.loadScriptRunner(this.activeConfig)) { + console.error('Error loading script runner') + return + } + } + try { + this.setIsLoading(this.activeConfig.name, true) + await this.call(`${this.scriptRunnerProfileName}${this.activeConfig.name}`, 'execute', script, filePath) + } catch (e) { + console.error('Error executing script', e) + } + this.setIsLoading(this.activeConfig.name, false) + + } + + async setErrorStatus(name: string, status: boolean, error: string) { + this.configurations.forEach((config) => { + if (config.name === name) { + config.errorStatus = status + config.error = error + } + }) + this.renderComponent() + } + + async setIsLoading(name: string, status: boolean) { + if (status) { + this.emit('statusChanged', { + key: 'loading', + type: 'info', + title: 'loading...' + }) + } else { + this.emit('statusChanged', { + key: 'none' + }) + } + this.configurations.forEach((config) => { + if (config.name === name) { + config.isLoading = status + } + }) + this.renderComponent() + } + + async dependencyError(data: any) { + console.log('Script runner dependency error: ', data) + let message = `Error loading dependencies: ` + if (isArray(data.data)) { + data.data.forEach((data: any) => { + message += `${data}` + }) + } + + const modal: AppModal = { + id: 'TemplatesSelection', + title: 'Missing dependencies', + message: `${message} \n\n You may need to setup a script engine for this workspace to load the correct dependencies. Do you want go to setup now?`, + okLabel: window._intl.formatMessage({ id: 'filePanel.ok' }), + cancelLabel: 'ignore' + } + const modalResult = await this.plugin.call('notification' as any, 'modal', modal) + if (modalResult) { + await this.plugin.call('menuicons', 'select', 'scriptRunnerBridge') + } else { + + } + } + + async log(data: any) { + this.emit('log', data) + } + + async warn(data: any) { + this.emit('warn', data) + } + + async error(data: any) { + this.emit('error', data) + } + + async info(data: any) { + this.emit('info', data) + } + + async loadCustomConfig(): Promise { + try { + const content = await this.plugin.call('fileManager', 'readFile', configFileName) + const parsed = JSON.parse(content) + this.customConfig = parsed + } catch (e) { + this.customConfig = { + defaultConfig: 'default', + customConfig: { + baseConfiguration: 'default', + dependencies: [] + } + } + } + + } + + async openCustomConfig() { + try { + await this.plugin.call('fileManager', 'open', '.remix/script.config.json') + } catch (e) { + + } + } + + async loadConfigurations() { + try { + const response = await axios.get(`${baseUrl}/projects.json?timestamp=${Date.now()}`); + this.configurations = response.data; + // find the default otherwise pick the first one as the active + this.configurations.forEach((config) => { + if (config.name === (this.customConfig.defaultConfig)) { + this.activeConfig = config; + } + }); + if (!this.activeConfig) { + this.activeConfig = this.configurations[0]; + } + } catch (error) { + console.error("Error fetching the projects data:", error); + } + + } + + async saveCustomConfig(content: ScriptRunnerConfig) { + if (content.customConfig.dependencies.length === 0 && content.defaultConfig === 'default') { + try { + const exists = await this.plugin.call('fileManager', 'exists', '.remix/script.config.json') + if (exists) { + await this.plugin.call('fileManager', 'remove', '.remix/script.config.json') + } + } catch (e) { + } + return + } + await this.plugin.call('fileManager', 'writeFile', '.remix/script.config.json', JSON.stringify(content, null, 2)) + } + + async activateCustomScriptRunner(config: customScriptRunnerConfig) { + try { + const result = await axios.post(customBuildUrl, config) + if (result.data.hash) { + + const newConfig: ProjectConfiguration = { + name: result.data.hash, + title: 'Custom configuration', + publish: true, + description: `Extension of ${config.baseConfiguration}`, + dependencies: config.dependencies, + replacements: {}, + errorStatus: false, + error: '', + isLoading: false + }; + this.configurations.push(newConfig) + this.renderComponent() + await this.loadScriptRunner(result.data.hash) + } + return result.data.hash + } catch (error) { + let message + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + console.log('Error status:', error.response.status); + console.log('Error data:', error.response.data); // This should give you the output being sent + console.log('Error headers:', error.response.headers); + + if (error.response.data.error) { + + if (isArray(error.response.data.error)) { + const message = `${error.response.data.error[0]}` + this.plugin.call('notification', 'alert', { + id: 'scriptalert', + message, + title: 'Error' + }) + throw new Error(message) + } + message = `${error.response.data.error}` + } + message = `Uknown error: ${error.response.data}` + this.plugin.call('notification', 'alert', { + id: 'scriptalert', + message, + title: 'Error' + }) + throw new Error(message) + } else if (error.request) { + // The request was made but no response was received + console.log('No response received:', error.request); + throw new Error('No response received') + } else { + // Something happened in setting up the request that triggered an Error + console.log('Error message:', error.message); + throw new Error(error.message) + } + + } + } + +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/tabs/web3-provider.js b/apps/remix-ide/src/app/tabs/web3-provider.js index 41bd9d3c27..39e5543c2e 100644 --- a/apps/remix-ide/src/app/tabs/web3-provider.js +++ b/apps/remix-ide/src/app/tabs/web3-provider.js @@ -86,7 +86,7 @@ export class Web3ProviderModule extends Plugin { try { resultFn(null, await provider.sendAsync(payload)) } catch (e) { - resultFn(e.error ? new Error(e.error) : new Error(e)) + resultFn(e.error ? e.error : e) } } else { reject(new Error('User denied permission')) diff --git a/apps/remix-ide/src/app/udapp/run-tab.tsx b/apps/remix-ide/src/app/udapp/run-tab.tsx index b513a76c2e..bc811f8aff 100644 --- a/apps/remix-ide/src/app/udapp/run-tab.tsx +++ b/apps/remix-ide/src/app/udapp/run-tab.tsx @@ -194,22 +194,7 @@ export class RunTab extends ViewPlugin { if (options['fork']) this.fork = options['fork'] } }, - provider: { - sendAsync (payload) { - return udapp.call(name, 'sendAsync', payload) - }, - async request (payload) { - try { - const requestResult = await udapp.call(name, 'sendAsync', payload) - if (requestResult.error) { - throw new Error(requestResult.error.message) - } - return requestResult.result - } catch (err) { - throw new Error(err.message) - } - } - } + provider: new Provider(udapp, name) }) } @@ -302,3 +287,43 @@ export class RunTab extends ViewPlugin { this.addInstance(address, contractObject.abi, contractObject.name) } } + +class Provider { + udapp: RunTab + name: string + constructor(udapp, name) { + this.udapp = udapp + this.name = name + } + sendAsync (payload) { + return this.udapp.call(this.name, 'sendAsync', payload) + } + request (payload): Promise { + return new Promise((resolve, reject) => { + this.udapp.call(this.name, 'sendAsync', payload).then((response) => { + if (response.error) { + reject(response.error.message) + } else { + resolve(response.result? response.result : response) + } + }).catch((err) => { + if (typeof err === 'string') { + reject(err) + } else if (err.error && err.error.message) { + reject(err.error.message) + } else if (err.error && typeof err.error === 'string') { + reject(err.error) + } else { + let e + try { + e = JSON.stringify(err) + } catch (e) { + reject('unknown error') + return + } + reject(e) + } + }) + }) + } +} diff --git a/apps/remix-ide/src/assets/fontawesome/css/all.css b/apps/remix-ide/src/assets/fontawesome/css/all.css index 59c32c20ed..8b1fd3fad7 100644 --- a/apps/remix-ide/src/assets/fontawesome/css/all.css +++ b/apps/remix-ide/src/assets/fontawesome/css/all.css @@ -2,26 +2,28 @@ font-family: var(--fa-style-family, "Font Awesome 6 Pro"); font-weight: var(--fa-style, 900); } -.fa, -.fa-classic, -.fa-sharp, -.fas, .fa-solid, -.far, .fa-regular, -.fasr, +.fa-brands, +.fas, +.far, +.fab, .fal, -.fa-light, -.fasl, .fat, -.fa-thin, -.fast, .fad, -.fa-duotone, .fass, +.fasr, +.fasl, +.fast, +.fasds, +.fa-light, +.fa-thin, +.fa-duotone, +.fa-sharp, +.fa-sharp-duotone, .fa-sharp-solid, -.fab, -.fa-brands { +.fa-classic, +.fa { -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; display: var(--fa-display, inline-block); @@ -31,14 +33,14 @@ text-rendering: auto; } .fas, -.fa-classic, -.fa-solid, .far, -.fa-regular, .fal, -.fa-light, .fat, -.fa-thin { +.fa-solid, +.fa-regular, +.fa-light, +.fa-thin, +.fa-classic { font-family: 'Font Awesome 6 Pro'; } .fab, @@ -50,6 +52,14 @@ .fa-duotone { font-family: 'Font Awesome 6 Duotone'; } +.fasds, +.fa-sharp-duotone { + font-family: 'Font Awesome 6 Sharp Duotone'; } + +.fasds, +.fa-sharp-duotone { + font-weight: 900; } + .fass, .fasr, .fasl, @@ -133,7 +143,7 @@ position: relative; } .fa-li { - left: calc(var(--fa-li-width, 2em) * -1); + left: calc(-1 * var(--fa-li-width, 2em)); position: absolute; text-align: center; width: var(--fa-li-width, 2em); @@ -155,118 +165,71 @@ margin-left: var(--fa-pull-margin, 0.3em); } .fa-beat { - -webkit-animation-name: fa-beat; - animation-name: fa-beat; - -webkit-animation-delay: var(--fa-animation-delay, 0s); - animation-delay: var(--fa-animation-delay, 0s); - -webkit-animation-direction: var(--fa-animation-direction, normal); - animation-direction: var(--fa-animation-direction, normal); - -webkit-animation-duration: var(--fa-animation-duration, 1s); - animation-duration: var(--fa-animation-duration, 1s); - -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); - animation-timing-function: var(--fa-animation-timing, ease-in-out); } + animation-name: fa-beat; + animation-delay: var(--fa-animation-delay, 0s); + animation-direction: var(--fa-animation-direction, normal); + animation-duration: var(--fa-animation-duration, 1s); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-timing-function: var(--fa-animation-timing, ease-in-out); } .fa-bounce { - -webkit-animation-name: fa-bounce; - animation-name: fa-bounce; - -webkit-animation-delay: var(--fa-animation-delay, 0s); - animation-delay: var(--fa-animation-delay, 0s); - -webkit-animation-direction: var(--fa-animation-direction, normal); - animation-direction: var(--fa-animation-direction, normal); - -webkit-animation-duration: var(--fa-animation-duration, 1s); - animation-duration: var(--fa-animation-duration, 1s); - -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); - animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); } + animation-name: fa-bounce; + animation-delay: var(--fa-animation-delay, 0s); + animation-direction: var(--fa-animation-direction, normal); + animation-duration: var(--fa-animation-duration, 1s); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); } .fa-fade { - -webkit-animation-name: fa-fade; - animation-name: fa-fade; - -webkit-animation-delay: var(--fa-animation-delay, 0s); - animation-delay: var(--fa-animation-delay, 0s); - -webkit-animation-direction: var(--fa-animation-direction, normal); - animation-direction: var(--fa-animation-direction, normal); - -webkit-animation-duration: var(--fa-animation-duration, 1s); - animation-duration: var(--fa-animation-duration, 1s); - -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); - animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); } + animation-name: fa-fade; + animation-delay: var(--fa-animation-delay, 0s); + animation-direction: var(--fa-animation-direction, normal); + animation-duration: var(--fa-animation-duration, 1s); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); } .fa-beat-fade { - -webkit-animation-name: fa-beat-fade; - animation-name: fa-beat-fade; - -webkit-animation-delay: var(--fa-animation-delay, 0s); - animation-delay: var(--fa-animation-delay, 0s); - -webkit-animation-direction: var(--fa-animation-direction, normal); - animation-direction: var(--fa-animation-direction, normal); - -webkit-animation-duration: var(--fa-animation-duration, 1s); - animation-duration: var(--fa-animation-duration, 1s); - -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); - animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); } + animation-name: fa-beat-fade; + animation-delay: var(--fa-animation-delay, 0s); + animation-direction: var(--fa-animation-direction, normal); + animation-duration: var(--fa-animation-duration, 1s); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); } .fa-flip { - -webkit-animation-name: fa-flip; - animation-name: fa-flip; - -webkit-animation-delay: var(--fa-animation-delay, 0s); - animation-delay: var(--fa-animation-delay, 0s); - -webkit-animation-direction: var(--fa-animation-direction, normal); - animation-direction: var(--fa-animation-direction, normal); - -webkit-animation-duration: var(--fa-animation-duration, 1s); - animation-duration: var(--fa-animation-duration, 1s); - -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); - animation-timing-function: var(--fa-animation-timing, ease-in-out); } + animation-name: fa-flip; + animation-delay: var(--fa-animation-delay, 0s); + animation-direction: var(--fa-animation-direction, normal); + animation-duration: var(--fa-animation-duration, 1s); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-timing-function: var(--fa-animation-timing, ease-in-out); } .fa-shake { - -webkit-animation-name: fa-shake; - animation-name: fa-shake; - -webkit-animation-delay: var(--fa-animation-delay, 0s); - animation-delay: var(--fa-animation-delay, 0s); - -webkit-animation-direction: var(--fa-animation-direction, normal); - animation-direction: var(--fa-animation-direction, normal); - -webkit-animation-duration: var(--fa-animation-duration, 1s); - animation-duration: var(--fa-animation-duration, 1s); - -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - -webkit-animation-timing-function: var(--fa-animation-timing, linear); - animation-timing-function: var(--fa-animation-timing, linear); } + animation-name: fa-shake; + animation-delay: var(--fa-animation-delay, 0s); + animation-direction: var(--fa-animation-direction, normal); + animation-duration: var(--fa-animation-duration, 1s); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-timing-function: var(--fa-animation-timing, linear); } .fa-spin { - -webkit-animation-name: fa-spin; - animation-name: fa-spin; - -webkit-animation-delay: var(--fa-animation-delay, 0s); - animation-delay: var(--fa-animation-delay, 0s); - -webkit-animation-direction: var(--fa-animation-direction, normal); - animation-direction: var(--fa-animation-direction, normal); - -webkit-animation-duration: var(--fa-animation-duration, 2s); - animation-duration: var(--fa-animation-duration, 2s); - -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - -webkit-animation-timing-function: var(--fa-animation-timing, linear); - animation-timing-function: var(--fa-animation-timing, linear); } + animation-name: fa-spin; + animation-delay: var(--fa-animation-delay, 0s); + animation-direction: var(--fa-animation-direction, normal); + animation-duration: var(--fa-animation-duration, 2s); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-timing-function: var(--fa-animation-timing, linear); } .fa-spin-reverse { --fa-animation-direction: reverse; } .fa-pulse, .fa-spin-pulse { - -webkit-animation-name: fa-spin; - animation-name: fa-spin; - -webkit-animation-direction: var(--fa-animation-direction, normal); - animation-direction: var(--fa-animation-direction, normal); - -webkit-animation-duration: var(--fa-animation-duration, 1s); - animation-duration: var(--fa-animation-duration, 1s); - -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - -webkit-animation-timing-function: var(--fa-animation-timing, steps(8)); - animation-timing-function: var(--fa-animation-timing, steps(8)); } + animation-name: fa-spin; + animation-direction: var(--fa-animation-direction, normal); + animation-duration: var(--fa-animation-duration, 1s); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-timing-function: var(--fa-animation-timing, steps(8)); } @media (prefers-reduced-motion: reduce) { .fa-beat, @@ -278,219 +241,97 @@ .fa-shake, .fa-spin, .fa-spin-pulse { - -webkit-animation-delay: -1ms; - animation-delay: -1ms; - -webkit-animation-duration: 1ms; - animation-duration: 1ms; - -webkit-animation-iteration-count: 1; - animation-iteration-count: 1; - -webkit-transition-delay: 0s; - transition-delay: 0s; - -webkit-transition-duration: 0s; - transition-duration: 0s; } } - -@-webkit-keyframes fa-beat { - 0%, 90% { - -webkit-transform: scale(1); - transform: scale(1); } - 45% { - -webkit-transform: scale(var(--fa-beat-scale, 1.25)); - transform: scale(var(--fa-beat-scale, 1.25)); } } + animation-delay: -1ms; + animation-duration: 1ms; + animation-iteration-count: 1; + transition-delay: 0s; + transition-duration: 0s; } } @keyframes fa-beat { 0%, 90% { - -webkit-transform: scale(1); - transform: scale(1); } + transform: scale(1); } 45% { - -webkit-transform: scale(var(--fa-beat-scale, 1.25)); - transform: scale(var(--fa-beat-scale, 1.25)); } } - -@-webkit-keyframes fa-bounce { - 0% { - -webkit-transform: scale(1, 1) translateY(0); - transform: scale(1, 1) translateY(0); } - 10% { - -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); - transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); } - 30% { - -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); - transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); } - 50% { - -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); - transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); } - 57% { - -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); - transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); } - 64% { - -webkit-transform: scale(1, 1) translateY(0); - transform: scale(1, 1) translateY(0); } - 100% { - -webkit-transform: scale(1, 1) translateY(0); - transform: scale(1, 1) translateY(0); } } + transform: scale(var(--fa-beat-scale, 1.25)); } } @keyframes fa-bounce { 0% { - -webkit-transform: scale(1, 1) translateY(0); - transform: scale(1, 1) translateY(0); } + transform: scale(1, 1) translateY(0); } 10% { - -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); - transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); } + transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); } 30% { - -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); - transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); } + transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); } 50% { - -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); - transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); } + transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); } 57% { - -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); - transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); } + transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); } 64% { - -webkit-transform: scale(1, 1) translateY(0); - transform: scale(1, 1) translateY(0); } + transform: scale(1, 1) translateY(0); } 100% { - -webkit-transform: scale(1, 1) translateY(0); - transform: scale(1, 1) translateY(0); } } - -@-webkit-keyframes fa-fade { - 50% { - opacity: var(--fa-fade-opacity, 0.4); } } + transform: scale(1, 1) translateY(0); } } @keyframes fa-fade { 50% { opacity: var(--fa-fade-opacity, 0.4); } } -@-webkit-keyframes fa-beat-fade { - 0%, 100% { - opacity: var(--fa-beat-fade-opacity, 0.4); - -webkit-transform: scale(1); - transform: scale(1); } - 50% { - opacity: 1; - -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); - transform: scale(var(--fa-beat-fade-scale, 1.125)); } } - @keyframes fa-beat-fade { 0%, 100% { opacity: var(--fa-beat-fade-opacity, 0.4); - -webkit-transform: scale(1); - transform: scale(1); } + transform: scale(1); } 50% { opacity: 1; - -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); - transform: scale(var(--fa-beat-fade-scale, 1.125)); } } - -@-webkit-keyframes fa-flip { - 50% { - -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); - transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } } + transform: scale(var(--fa-beat-fade-scale, 1.125)); } } @keyframes fa-flip { 50% { - -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); - transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } } - -@-webkit-keyframes fa-shake { - 0% { - -webkit-transform: rotate(-15deg); - transform: rotate(-15deg); } - 4% { - -webkit-transform: rotate(15deg); - transform: rotate(15deg); } - 8%, 24% { - -webkit-transform: rotate(-18deg); - transform: rotate(-18deg); } - 12%, 28% { - -webkit-transform: rotate(18deg); - transform: rotate(18deg); } - 16% { - -webkit-transform: rotate(-22deg); - transform: rotate(-22deg); } - 20% { - -webkit-transform: rotate(22deg); - transform: rotate(22deg); } - 32% { - -webkit-transform: rotate(-12deg); - transform: rotate(-12deg); } - 36% { - -webkit-transform: rotate(12deg); - transform: rotate(12deg); } - 40%, 100% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); } } + transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } } @keyframes fa-shake { 0% { - -webkit-transform: rotate(-15deg); - transform: rotate(-15deg); } + transform: rotate(-15deg); } 4% { - -webkit-transform: rotate(15deg); - transform: rotate(15deg); } + transform: rotate(15deg); } 8%, 24% { - -webkit-transform: rotate(-18deg); - transform: rotate(-18deg); } + transform: rotate(-18deg); } 12%, 28% { - -webkit-transform: rotate(18deg); - transform: rotate(18deg); } + transform: rotate(18deg); } 16% { - -webkit-transform: rotate(-22deg); - transform: rotate(-22deg); } + transform: rotate(-22deg); } 20% { - -webkit-transform: rotate(22deg); - transform: rotate(22deg); } + transform: rotate(22deg); } 32% { - -webkit-transform: rotate(-12deg); - transform: rotate(-12deg); } + transform: rotate(-12deg); } 36% { - -webkit-transform: rotate(12deg); - transform: rotate(12deg); } + transform: rotate(12deg); } 40%, 100% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); } } - -@-webkit-keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); } } + transform: rotate(0deg); } } @keyframes fa-spin { 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); } + transform: rotate(0deg); } 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); } } + transform: rotate(360deg); } } .fa-rotate-90 { - -webkit-transform: rotate(90deg); - transform: rotate(90deg); } + transform: rotate(90deg); } .fa-rotate-180 { - -webkit-transform: rotate(180deg); - transform: rotate(180deg); } + transform: rotate(180deg); } .fa-rotate-270 { - -webkit-transform: rotate(270deg); - transform: rotate(270deg); } + transform: rotate(270deg); } .fa-flip-horizontal { - -webkit-transform: scale(-1, 1); - transform: scale(-1, 1); } + transform: scale(-1, 1); } .fa-flip-vertical { - -webkit-transform: scale(1, -1); - transform: scale(1, -1); } + transform: scale(1, -1); } .fa-flip-both, .fa-flip-horizontal.fa-flip-vertical { - -webkit-transform: scale(-1, -1); - transform: scale(-1, -1); } + transform: scale(-1, -1); } .fa-rotate-by { - -webkit-transform: rotate(var(--fa-rotate-angle, 0)); - transform: rotate(var(--fa-rotate-angle, 0)); } + transform: rotate(var(--fa-rotate-angle, 0)); } .fa-stack { display: inline-block; @@ -1799,6 +1640,7 @@ readers do not read off random characters that represent icons */ .fa-diamond-half::before { content: "\e5b7"; } .fa-diamond-half-stroke::before { content: "\e5b8"; } .fa-diamond-turn-right::before { content: "\f5eb"; } +.fa-diamonds-4::before { content: "\e68b"; } .fa-dice::before { content: "\f522"; } .fa-dice-d10::before { content: "\f6cd"; } .fa-dice-d12::before { content: "\f6ce"; } @@ -2383,6 +2225,7 @@ readers do not read off random characters that represent icons */ .fa-globe-pointer::before { content: "\e60e"; } .fa-globe-snow::before { content: "\f7a3"; } .fa-globe-stand::before { content: "\f5f6"; } +.fa-globe-wifi::before { content: "\e685"; } .fa-glove-boxing::before { content: "\f438"; } .fa-goal-net::before { content: "\e3ab"; } .fa-golf-ball::before { content: "\f450"; } @@ -2687,6 +2530,7 @@ readers do not read off random characters that represent icons */ .fa-humidity::before { content: "\f750"; } .fa-hundred-points::before { content: "\e41c"; } .fa-hurricane::before { content: "\f751"; } +.fa-hydra::before { content: "\e686"; } .fa-hyphen::before { content: "\2d"; } .fa-i::before { content: "\49"; } .fa-i-cursor::before { content: "\f246"; } @@ -2854,6 +2698,7 @@ readers do not read off random characters that represent icons */ .fa-lightbulb-exclamation::before { content: "\f671"; } .fa-lightbulb-exclamation-on::before { content: "\e1ca"; } .fa-lightbulb-gear::before { content: "\e5fd"; } +.fa-lightbulb-message::before { content: "\e687"; } .fa-lightbulb-on::before { content: "\f672"; } .fa-lightbulb-slash::before { content: "\f673"; } .fa-lighthouse::before { content: "\e612"; } @@ -3208,6 +3053,7 @@ readers do not read off random characters that represent icons */ .fa-octagon-minus::before { content: "\f308"; } .fa-octagon-plus::before { content: "\f301"; } .fa-octagon-xmark::before { content: "\f2f0"; } +.fa-octopus::before { content: "\e688"; } .fa-oil-can::before { content: "\f613"; } .fa-oil-can-drip::before { content: "\e205"; } .fa-oil-temp::before { content: "\f614"; } @@ -4196,9 +4042,12 @@ readers do not read off random characters that represent icons */ .fa-table::before { content: "\f0ce"; } .fa-table-cells::before { content: "\f00a"; } .fa-table-cells-column-lock::before { content: "\e678"; } +.fa-table-cells-column-unlock::before { content: "\e690"; } .fa-table-cells-large::before { content: "\f009"; } .fa-table-cells-lock::before { content: "\e679"; } .fa-table-cells-row-lock::before { content: "\e67a"; } +.fa-table-cells-row-unlock::before { content: "\e691"; } +.fa-table-cells-unlock::before { content: "\e692"; } .fa-table-columns::before { content: "\f0db"; } .fa-table-layout::before { content: "\e290"; } .fa-table-list::before { content: "\f00b"; } @@ -4311,9 +4160,11 @@ readers do not read off random characters that represent icons */ .fa-theta::before { content: "\f69e"; } .fa-thought-bubble::before { content: "\e32e"; } .fa-thumb-tack::before { content: "\f08d"; } +.fa-thumb-tack-slash::before { content: "\e68f"; } .fa-thumbs-down::before { content: "\f165"; } .fa-thumbs-up::before { content: "\f164"; } .fa-thumbtack::before { content: "\f08d"; } +.fa-thumbtack-slash::before { content: "\e68f"; } .fa-thunderstorm::before { content: "\f76c"; } .fa-thunderstorm-moon::before { content: "\f76d"; } .fa-thunderstorm-sun::before { content: "\f76e"; } @@ -4556,6 +4407,7 @@ readers do not read off random characters that represent icons */ .fa-user-alt::before { content: "\f406"; } .fa-user-alt-slash::before { content: "\f4fa"; } .fa-user-astronaut::before { content: "\f4fb"; } +.fa-user-beard-bolt::before { content: "\e689"; } .fa-user-bounty-hunter::before { content: "\e2bf"; } .fa-user-chart::before { content: "\f6a3"; } .fa-user-check::before { content: "\f4fc"; } @@ -4584,6 +4436,7 @@ readers do not read off random characters that represent icons */ .fa-user-hard-hat::before { content: "\f82c"; } .fa-user-headset::before { content: "\f82d"; } .fa-user-helmet-safety::before { content: "\f82c"; } +.fa-user-hoodie::before { content: "\e68a"; } .fa-user-injured::before { content: "\f728"; } .fa-user-large::before { content: "\f406"; } .fa-user-large-slash::before { content: "\f4fa"; } @@ -4833,7 +4686,7 @@ readers do not read off random characters that represent icons */ border-width: 0; } /*! - * Font Awesome Pro 6.5.2 by @fontawesome - https://fontawesome.com + * Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license (Commercial License) * Copyright 2024 Fonticons, Inc. */ @@ -4954,6 +4807,9 @@ readers do not read off random characters that represent icons */ .fa-jxl:before { content: "\e67b"; } +.fa-dart-lang:before { + content: "\e693"; } + .fa-hire-a-helper:before { content: "\f3b0"; } @@ -5893,6 +5749,9 @@ readers do not read off random characters that represent icons */ .fa-twitch:before { content: "\f1e8"; } +.fa-flutter:before { + content: "\e694"; } + .fa-ravelry:before { content: "\f2d9"; } @@ -6428,7 +6287,7 @@ readers do not read off random characters that represent icons */ content: "\f3f6"; } /*! - * Font Awesome Pro 6.5.2 by @fontawesome - https://fontawesome.com + * Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license (Commercial License) * Copyright 2024 Fonticons, Inc. */ @@ -6448,7 +6307,7 @@ readers do not read off random characters that represent icons */ font-weight: 300; } /*! - * Font Awesome Pro 6.5.2 by @fontawesome - https://fontawesome.com + * Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license (Commercial License) * Copyright 2024 Fonticons, Inc. */ @@ -6468,7 +6327,7 @@ readers do not read off random characters that represent icons */ font-weight: 400; } /*! - * Font Awesome Pro 6.5.2 by @fontawesome - https://fontawesome.com + * Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license (Commercial License) * Copyright 2024 Fonticons, Inc. */ @@ -6488,7 +6347,7 @@ readers do not read off random characters that represent icons */ font-weight: 900; } /*! - * Font Awesome Pro 6.5.2 by @fontawesome - https://fontawesome.com + * Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license (Commercial License) * Copyright 2024 Fonticons, Inc. */ @@ -7505,6 +7364,9 @@ readers do not read off random characters that represent icons */ .fad.fa-trillium::after, .fa-duotone.fa-trillium::after { content: "\e588\e588"; } +.fad.fa-table-cells-unlock::after, .fa-duotone.fa-table-cells-unlock::after { + content: "\e692\e692"; } + .fad.fa-music-slash::after, .fa-duotone.fa-music-slash::after { content: "\f8d1\f8d1"; } @@ -8084,6 +7946,9 @@ readers do not read off random characters that represent icons */ .fad.fa-phone-square-alt::after, .fa-duotone.fa-phone-square-alt::after { content: "\f87b\f87b"; } +.fad.fa-user-beard-bolt::after, .fa-duotone.fa-user-beard-bolt::after { + content: "\e689\e689"; } + .fad.fa-cart-plus::after, .fa-duotone.fa-cart-plus::after { content: "\f217\f217"; } @@ -9581,6 +9446,9 @@ readers do not read off random characters that represent icons */ .fad.fa-coffin-cross::after, .fa-duotone.fa-coffin-cross::after { content: "\e051\e051"; } +.fad.fa-octopus::after, .fa-duotone.fa-octopus::after { + content: "\e688\e688"; } + .fad.fa-spell-check::after, .fa-duotone.fa-spell-check::after { content: "\f891\f891"; } @@ -9770,6 +9638,12 @@ readers do not read off random characters that represent icons */ .fad.fa-passport::after, .fa-duotone.fa-passport::after { content: "\f5ab\f5ab"; } +.fad.fa-thumbtack-slash::after, .fa-duotone.fa-thumbtack-slash::after { + content: "\e68f\e68f"; } + +.fad.fa-thumb-tack-slash::after, .fa-duotone.fa-thumb-tack-slash::after { + content: "\e68f\e68f"; } + .fad.fa-inbox-in::after, .fa-duotone.fa-inbox-in::after { content: "\f310\f310"; } @@ -11090,6 +10964,9 @@ readers do not read off random characters that represent icons */ .fad.fa-ufo-beam::after, .fa-duotone.fa-ufo-beam::after { content: "\e048\e048"; } +.fad.fa-hydra::after, .fa-duotone.fa-hydra::after { + content: "\e686\e686"; } + .fad.fa-circle-caret-up::after, .fa-duotone.fa-circle-caret-up::after { content: "\f331\f331"; } @@ -11783,6 +11660,9 @@ readers do not read off random characters that represent icons */ .fad.fa-podium::after, .fa-duotone.fa-podium::after { content: "\f680\f680"; } +.fad.fa-diamonds-4::after, .fa-duotone.fa-diamonds-4::after { + content: "\e68b\e68b"; } + .fad.fa-memo-circle-check::after, .fa-duotone.fa-memo-circle-check::after { content: "\e1d9\e1d9"; } @@ -15038,6 +14918,9 @@ readers do not read off random characters that represent icons */ .fad.fa-pipe-valve::after, .fa-duotone.fa-pipe-valve::after { content: "\e439\e439"; } +.fad.fa-lightbulb-message::after, .fa-duotone.fa-lightbulb-message::after { + content: "\e687\e687"; } + .fad.fa-arrow-up-from-arc::after, .fa-duotone.fa-arrow-up-from-arc::after { content: "\e4b4\e4b4"; } @@ -15554,6 +15437,9 @@ readers do not read off random characters that represent icons */ .fad.fa-oil-well::after, .fa-duotone.fa-oil-well::after { content: "\e532\e532"; } +.fad.fa-table-cells-column-unlock::after, .fa-duotone.fa-table-cells-column-unlock::after { + content: "\e690\e690"; } + .fad.fa-person-simple::after, .fa-duotone.fa-person-simple::after { content: "\e220\e220"; } @@ -15851,6 +15737,9 @@ readers do not read off random characters that represent icons */ .fad.fa-toolbox::after, .fa-duotone.fa-toolbox::after { content: "\f552\f552"; } +.fad.fa-globe-wifi::after, .fa-duotone.fa-globe-wifi::after { + content: "\e685\e685"; } + .fad.fa-envelope-dot::after, .fa-duotone.fa-envelope-dot::after { content: "\e16f\e16f"; } @@ -16586,6 +16475,9 @@ readers do not read off random characters that represent icons */ .fad.fa-transporter-2::after, .fa-duotone.fa-transporter-2::after { content: "\e044\e044"; } +.fad.fa-user-hoodie::after, .fa-duotone.fa-user-hoodie::after { + content: "\e68a\e68a"; } + .fad.fa-hands-holding-diamond::after, .fa-duotone.fa-hands-holding-diamond::after { content: "\f47c\f47c"; } @@ -19121,6 +19013,9 @@ readers do not read off random characters that represent icons */ .fad.fa-brain-circuit::after, .fa-duotone.fa-brain-circuit::after { content: "\e0c6\e0c6"; } +.fad.fa-table-cells-row-unlock::after, .fa-duotone.fa-table-cells-row-unlock::after { + content: "\e691\e691"; } + .fad.fa-user-injured::after, .fa-duotone.fa-user-injured::after { content: "\f728\f728"; } @@ -19394,6 +19289,7 @@ readers do not read off random characters that represent icons */ .fak.fa-fa-dock-l::before, .fa-kit.fa-fa-dock-l::before { content: "\e007"; } .fak.fa-fa-dock-r::before, .fa-kit.fa-fa-dock-r::before { content: "\e006"; } .fak.fa-lexon::before, .fa-kit.fa-lexon::before { content: "\e004"; } +.fak.fa-solid-gear-circle-play::before, .fa-kit.fa-solid-gear-circle-play::before { content: "\e009"; } .fak.fa-solidity-mono::before, .fa-kit.fa-solidity-mono::before { content: "\e005"; } .fak.fa-ts-logo::before, .fa-kit.fa-ts-logo::before { content: "\e003"; } .fak.fa-vyper2::before, .fa-kit.fa-vyper2::before { content: "\e002"; } diff --git a/apps/remix-ide/src/assets/fontawesome/webfonts/custom-icons.ttf b/apps/remix-ide/src/assets/fontawesome/webfonts/custom-icons.ttf index cd8888a51e..e4c5fc4700 100644 Binary files a/apps/remix-ide/src/assets/fontawesome/webfonts/custom-icons.ttf and b/apps/remix-ide/src/assets/fontawesome/webfonts/custom-icons.ttf differ diff --git a/apps/remix-ide/src/assets/fontawesome/webfonts/custom-icons.woff2 b/apps/remix-ide/src/assets/fontawesome/webfonts/custom-icons.woff2 index d107b4d1c3..8ca1dcdad3 100644 Binary files a/apps/remix-ide/src/assets/fontawesome/webfonts/custom-icons.woff2 and b/apps/remix-ide/src/assets/fontawesome/webfonts/custom-icons.woff2 differ diff --git a/apps/remix-ide/src/assets/img/solid-gear-circle-play.svg b/apps/remix-ide/src/assets/img/solid-gear-circle-play.svg new file mode 100644 index 0000000000..6345426e91 --- /dev/null +++ b/apps/remix-ide/src/assets/img/solid-gear-circle-play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/remix-ide/src/blockchain/blockchain.tsx b/apps/remix-ide/src/blockchain/blockchain.tsx index 3690b3c122..6ccbaed7ac 100644 --- a/apps/remix-ide/src/blockchain/blockchain.tsx +++ b/apps/remix-ide/src/blockchain/blockchain.tsx @@ -247,7 +247,7 @@ export class Blockchain extends Plugin { args, (error, data) => { if (error) { - return statusCb(`creation of ${selectedContract.name} errored: ${error.message ? error.message : error}`) + return statusCb(`creation of ${selectedContract.name} errored: ${error.message ? error.message : error.error ? error.error : error}`) } statusCb(`creation of ${selectedContract.name} pending...`) @@ -272,7 +272,7 @@ export class Blockchain extends Plugin { selectedContract.bytecodeLinkReferences, (error, data) => { if (error) { - return statusCb(`creation of ${selectedContract.name} errored: ${error.message ? error.message : error}`) + return statusCb(`creation of ${selectedContract.name} errored: ${error.message ? error.message : error.error ? error.error : error}`) } statusCb(`creation of ${selectedContract.name} pending...`) @@ -485,7 +485,7 @@ export class Blockchain extends Plugin { this.runTx({ data: data, useCall: false }, confirmationCb, continueCb, promptCb, (error, txResult, address) => { if (error) { - return finalCb(`creation of ${selectedContract.name} errored: ${error.message ? error.message : error}`) + return finalCb(`creation of ${selectedContract.name} errored: ${error.message ? error.message : error.error ? error.error : error}`) } if (txResult.receipt.status === false || txResult.receipt.status === '0x0' || txResult.receipt.status === 0) { return finalCb(`creation of ${selectedContract.name} errored: transaction execution failed`) @@ -614,7 +614,7 @@ export class Blockchain extends Plugin { callType, (error, data) => { if (error) { - return logCallback(`${logMsg} errored: ${error.message ? error.message : error}`) + return logCallback(`${logMsg} errored: ${error.message ? error.message : error.error ? error.error : error}`) } if (!lookupOnly) { logCallback(`${logMsg} pending ... `) @@ -631,7 +631,7 @@ export class Blockchain extends Plugin { const useCall = funABI.stateMutability === 'view' || funABI.stateMutability === 'pure' this.runTx({ to: address, data, useCall }, confirmationCb, continueCb, promptCb, (error, txResult, _address, returnValue) => { if (error) { - return logCallback(`${logMsg} errored: ${error.message ? error.message : error}`) + return logCallback(`${logMsg} errored: ${error.message ? error.message : error.error ? error.error : error}`) } if (lookupOnly) { outputCb(returnValue) diff --git a/apps/remix-ide/src/remixAppManager.js b/apps/remix-ide/src/remixAppManager.js index 3a11908675..9d6938f134 100644 --- a/apps/remix-ide/src/remixAppManager.js +++ b/apps/remix-ide/src/remixAppManager.js @@ -25,6 +25,7 @@ let requiredModules = [ 'blockchain', 'web3Provider', 'scriptRunner', + 'scriptRunnerBridge', 'fetchAndCompile', 'mainPanel', 'hiddenPanel', @@ -109,6 +110,10 @@ const isVM = (name) => { return name.startsWith('vm') } +const isScriptRunner = (name) => { + return name.startsWith('scriptRunner') +} + export function isNative(name) { // nativePlugin allows to bypass the permission request @@ -142,7 +147,7 @@ export function isNative(name) { 'walletconnect', 'contract-verification' ] - return nativePlugins.includes(name) || requiredModules.includes(name) || isInjectedProvider(name) || isVM(name) + return nativePlugins.includes(name) || requiredModules.includes(name) || isInjectedProvider(name) || isVM(name) || isScriptRunner(name) } /** @@ -195,6 +200,8 @@ export class RemixAppManager extends PluginManager { } } await this.toggleActive(name) + }else{ + console.log('cannot deactivate', name) } } @@ -253,7 +260,7 @@ export class RemixAppManager extends PluginManager { isRequired(name) { // excluding internal use plugins - return requiredModules.includes(name) || isInjectedProvider(name) || isVM(name) + return requiredModules.includes(name) || isInjectedProvider(name) || isVM(name) || isScriptRunner(name) } async registeredPlugins() { @@ -302,6 +309,7 @@ export class RemixAppManager extends PluginManager { return plugins.map(plugin => { if (plugin.name === 'dgit' && Registry.getInstance().get('platform').api.isDesktop()) { plugin.url = 'https://dgit4-76cc9.web.app/' } if (plugin.name === testPluginName) plugin.url = testPluginUrl + //console.log('plugin', plugin) return new IframePlugin(plugin) }) } diff --git a/apps/remix-ide/src/remixEngine.js b/apps/remix-ide/src/remixEngine.js index c6cbfa21c0..f81bcb5037 100644 --- a/apps/remix-ide/src/remixEngine.js +++ b/apps/remix-ide/src/remixEngine.js @@ -30,7 +30,6 @@ export class RemixEngine extends Engine { if (name === 'remixAI') return { queueTimeout: 60000 * 20 } if (name === 'cookbookdev') return { queueTimeout: 60000 * 3 } if (name === 'contentImport') return { queueTimeout: 60000 * 3 } - return { queueTimeout: 10000 } } diff --git a/apps/remix-ide/team-best-practices.md b/apps/remix-ide/team-best-practices.md index 4918dc0e09..573b003dd9 100644 --- a/apps/remix-ide/team-best-practices.md +++ b/apps/remix-ide/team-best-practices.md @@ -175,7 +175,7 @@ Before starting coding, we should ensure all devs / contributors are aware of: # Coding best practices - - https://github.com/ethereum/remix-project/blob/master/best-practices.md + - https://github.com/ethereum/remix-project/blob/master/team-best-practices.md --- diff --git a/apps/remixdesktop/test/nighwatch.app.ts b/apps/remixdesktop/test/nighwatch.app.ts index ee439cccf7..4498db0f52 100644 --- a/apps/remixdesktop/test/nighwatch.app.ts +++ b/apps/remixdesktop/test/nighwatch.app.ts @@ -42,7 +42,7 @@ module.exports = { selenium_host: 'localhost', globals: { waitForConditionTimeout: 10000, - asyncHookTimeout: 100000 + asyncHookTimeout: 30000 }, screenshots: { enabled: true, diff --git a/libs/remix-api/src/lib/plugins/fileSystem-api.ts b/libs/remix-api/src/lib/plugins/fileSystem-api.ts index 84210f2f7d..7d87220db1 100644 --- a/libs/remix-api/src/lib/plugins/fileSystem-api.ts +++ b/libs/remix-api/src/lib/plugins/fileSystem-api.ts @@ -9,5 +9,6 @@ export interface IExtendedFileSystem extends IFileSystem { refresh(): Promise hasGitSubmodules(): Promise isGitRepo(): Promise + exists(file: string): Promise }; } \ No newline at end of file diff --git a/libs/remix-api/src/lib/plugins/menuicons-api.ts b/libs/remix-api/src/lib/plugins/menuicons-api.ts new file mode 100644 index 0000000000..41f5715c02 --- /dev/null +++ b/libs/remix-api/src/lib/plugins/menuicons-api.ts @@ -0,0 +1,14 @@ +import { IFilePanel } from '@remixproject/plugin-api' +import { Profile, StatusEvents } from '@remixproject/plugin-utils' + +export interface IMenuIconsApi { + events: { + toggleContent: (name: string) => void, + showContent: (name: string) => void + } & StatusEvents + methods: { + select: (name: string) => void + linkContent: (profile: Profile) => void + unlinkContent: (profile: Profile) => void + } +} diff --git a/libs/remix-api/src/lib/remix-api.ts b/libs/remix-api/src/lib/remix-api.ts index 5b400d445f..7df4dc7104 100644 --- a/libs/remix-api/src/lib/remix-api.ts +++ b/libs/remix-api/src/lib/remix-api.ts @@ -15,6 +15,7 @@ import { ILayoutApi } from "./plugins/layout-api" import { IMatomoApi } from "./plugins/matomo-api" import { IRemixAI } from "./plugins/remixai-api" import { IRemixAID } from "./plugins/remixAIDesktop-api" +import { IMenuIconsApi } from "./plugins/menuicons-api" import { IDgitPlugin } from "./plugins/dgitplugin-api" import { Api } from "@remixproject/plugin-utils"; import { IPopupPanelAPI } from "./plugins/popuppanel-api" @@ -37,6 +38,7 @@ export interface ICustomRemixApi extends IRemixApi { pinnedPanel: IPinnedPanelApi layout: ILayoutApi matomo: IMatomoApi + menuicons: IMenuIconsApi remixAI: IRemixAI, remixAID: IRemixAID } diff --git a/libs/remix-lib/src/execution/txRunnerWeb3.ts b/libs/remix-lib/src/execution/txRunnerWeb3.ts index 0cdb0ee978..b2c46feff4 100644 --- a/libs/remix-lib/src/execution/txRunnerWeb3.ts +++ b/libs/remix-lib/src/execution/txRunnerWeb3.ts @@ -66,7 +66,7 @@ export class TxRunnerWeb3 { const res = await (this.getWeb3() as any).eth.personal.sendTransaction({ ...tx, value }, { checkRevertBeforeSending: false, ignoreGasPricing: true }) cb(null, res.transactionHash) } catch (e) { - console.log(`Send transaction failed: ${e.message} . if you use an injected provider, please check it is properly unlocked. `) + console.log(`Send transaction failed: ${e.message || e.error} . if you use an injected provider, please check it is properly unlocked. `) // in case the receipt is available, we consider that only the execution failed but the transaction went through. // So we don't consider this to be an error. if (e.receipt) cb(null, e.receipt.transactionHash) @@ -82,6 +82,10 @@ export class TxRunnerWeb3 { const res = await this.getWeb3().eth.sendTransaction(tx, null, { checkRevertBeforeSending: false, ignoreGasPricing: true }) cb(null, res.transactionHash) } catch (e) { + if (!e.message) e.message = '' + if (e.error) { + e.message = e.message + ' ' + e.error + } console.log(`Send transaction failed: ${e.message} . if you use an injected provider, please check it is properly unlocked. `) // in case the receipt is available, we consider that only the execution failed but the transaction went through. // So we don't consider this to be an error. diff --git a/libs/remix-simulator/src/methods/accounts.ts b/libs/remix-simulator/src/methods/accounts.ts index 6c99565455..c28818c8e6 100644 --- a/libs/remix-simulator/src/methods/accounts.ts +++ b/libs/remix-simulator/src/methods/accounts.ts @@ -76,6 +76,7 @@ export class Web3Accounts { methods (): Record { return { + eth_requestAccounts: this.eth_requestAccounts.bind(this), eth_accounts: this.eth_accounts.bind(this), eth_getBalance: this.eth_getBalance.bind(this), eth_sign: this.eth_sign.bind(this), @@ -85,6 +86,10 @@ export class Web3Accounts { } } + eth_requestAccounts (_payload, cb) { + return cb(null, Object.keys(this.accounts)) + } + eth_accounts (_payload, cb) { return cb(null, Object.keys(this.accounts)) } diff --git a/libs/remix-simulator/src/methods/transactions.ts b/libs/remix-simulator/src/methods/transactions.ts index 714852f693..d2bf5f6b2b 100644 --- a/libs/remix-simulator/src/methods/transactions.ts +++ b/libs/remix-simulator/src/methods/transactions.ts @@ -308,7 +308,6 @@ export class Transactions { const txBlock = this.vmContext.blockByTxHash[receipt.transactionHash] const tx = this.vmContext.txByHash[receipt.transactionHash] - // TODO: params to add later const r: Record = { blockHash: bytesToHex(txBlock.hash()), @@ -322,11 +321,10 @@ export class Transactions { input: receipt.input, nonce: bigIntToHex(tx.nonce), transactionIndex: this.TX_INDEX, - value: bigIntToHex(tx.value) - // "value":"0xf3dbb76162000" // 4290000000000000 - // "v": "0x25", // 37 - // "r": "0x1b5e176d927f8e9ab405058b2d2457392da3e20f328b16ddabcebc33eaac5fea", - // "s": "0x4ba69724e8f69de52f0125ad8b3c5c2cef33019bac3249e2c0a2192766d1721c" + value: bigIntToHex(tx.value), + v: bigIntToHex(tx.v), + r: bigIntToHex(tx.r), + s: bigIntToHex(tx.s) } if (receipt.to) { diff --git a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx index ab8de7ec34..d487090522 100644 --- a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx +++ b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx @@ -825,10 +825,6 @@ export const EditorUI = (props: EditorUIProps) => { label: intl.formatMessage({ id: 'editor.explainFunction' }), contextMenuOrder: 1, // choose the order contextMenuGroupId: 'gtp', // create a new grouping - keybindings: [ - // Keybinding for Ctrl + Shift + E - monacoRef.current.KeyMod.CtrlCmd | monacoRef.current.KeyMod.Shift | monacoRef.current.KeyCode.KeyE - ], run: async () => { const file = await props.plugin.call('fileManager', 'getCurrentFile') const context = await props.plugin.call('fileManager', 'readFile', file) @@ -996,6 +992,9 @@ export const EditorUI = (props: EditorUIProps) => { // Allow JSON schema requests monacoRef.current.languages.json.jsonDefaults.setDiagnosticsOptions({ enableSchemaRequest: true }) + // hide the module resolution error. We have to remove this when we know how to properly resolve imports. + monacoRef.current.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ diagnosticCodesToIgnore: [2792]}) + // Register a tokens provider for the language monacoRef.current.languages.setMonarchTokensProvider('remix-solidity', solidityTokensProvider as any) monacoRef.current.languages.setLanguageConfiguration('remix-solidity', solidityLanguageConfig as any) diff --git a/libs/remix-ui/home-tab/src/lib/components/homeTabTitle.tsx b/libs/remix-ui/home-tab/src/lib/components/homeTabTitle.tsx index b4049a661c..e3779944b9 100644 --- a/libs/remix-ui/home-tab/src/lib/components/homeTabTitle.tsx +++ b/libs/remix-ui/home-tab/src/lib/components/homeTabTitle.tsx @@ -46,7 +46,7 @@ const iconButtons: HometabIconSection[] = [ { textToolip: , matomoTrackingEntry: ['trackEvent', 'hometab', 'socialmedia', 'discord'], - urlLink: 'https://discord.gg/mh9hFCKkEq', + urlLink: 'https://discord.com/invite/nfv6ZYjAeP', iconClass: 'fa-discord', placement: 'top' } diff --git a/libs/remix-ui/run-tab/src/lib/actions/deploy.ts b/libs/remix-ui/run-tab/src/lib/actions/deploy.ts index dd08d62a36..2c2ee360c4 100644 --- a/libs/remix-ui/run-tab/src/lib/actions/deploy.ts +++ b/libs/remix-ui/run-tab/src/lib/actions/deploy.ts @@ -119,10 +119,19 @@ const getConfirmationCb = (plugin: RunTab, dispatch: React.Dispatch, confir export const continueHandler = (dispatch: React.Dispatch, gasEstimationPrompt: (msg: string) => JSX.Element, error, continueTxExecution, cancelCb) => { if (error) { - let msg = typeof error !== 'string' ? error.message : error + let msg = '' + if (typeof error === 'string') { + msg = error + } if (error && error.innerError) { msg += '\n' + error.innerError } + if (error && error.message) { + msg += '\n' + error.message + } + if (error && error.error) { + msg += '\n' + error.error + } if (msg.includes('invalid opcode')) msg += '\nThe EVM version used by the selected environment is not compatible with the compiler EVM version.' diff --git a/libs/remix-ui/scriptrunner/src/index.ts b/libs/remix-ui/scriptrunner/src/index.ts new file mode 100644 index 0000000000..a1683b6b22 --- /dev/null +++ b/libs/remix-ui/scriptrunner/src/index.ts @@ -0,0 +1,2 @@ +export { ScriptRunnerUI } from './lib/script-runner-ui'; +export * from './types'; \ No newline at end of file diff --git a/libs/remix-ui/scriptrunner/src/lib/custom-script-runner.tsx b/libs/remix-ui/scriptrunner/src/lib/custom-script-runner.tsx new file mode 100644 index 0000000000..be0959bd5b --- /dev/null +++ b/libs/remix-ui/scriptrunner/src/lib/custom-script-runner.tsx @@ -0,0 +1,168 @@ +import React, { useEffect, useState } from "react"; +import { customScriptRunnerConfig, Dependency, ProjectConfiguration } from "../types"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faToggleOff, faToggleOn, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { CustomTooltip } from "@remix-ui/helper"; + +export interface ScriptRunnerUIProps { + publishedConfigurations: ProjectConfiguration[]; + openCustomConfig: () => any; + saveCustomConfig(content: customScriptRunnerConfig): void; + activateCustomScriptRunner(config: customScriptRunnerConfig): Promise; + customConfig: customScriptRunnerConfig; +} + +export const CustomScriptRunner = (props: ScriptRunnerUIProps) => { + const [dependencies, setDependencies] = useState([]); + const [name, setName] = useState(''); + const [alias, setAlias] = useState(''); + const [version, setVersion] = useState(''); + const [baseConfig, setBaseConfig] = useState('default'); + const [loading, setLoading] = useState(false); + const [useRequire, setUseRequire] = useState(false) + + const { customConfig } = props; + + useEffect(() => { + if (!customConfig) return; + setDependencies(customConfig.dependencies); + setBaseConfig(customConfig.baseConfiguration); + },[customConfig]) + + const handleAddDependency = () => { + if (name.trim() && version.trim()) { + const newDependency: Dependency = { name, version, require: useRequire, alias }; + setDependencies([...dependencies, newDependency]); + setName(''); + setVersion(''); + } else { + alert('Please fill out both name and version.'); + } + }; + + const handleRemoveDependency = (index: number) => { + const updatedDependencies = dependencies.filter((_, i) => i !== index); + setDependencies(updatedDependencies); + }; + + const handleSaveToFile = () => { + const fileData = JSON.stringify(dependencies, null, 2); + console.log(fileData, baseConfig); + const customConfig: customScriptRunnerConfig = { baseConfiguration: baseConfig, dependencies }; + console.log(customConfig); + props.saveCustomConfig(customConfig); + }; + + const openConfig = async () => { + const fileData: customScriptRunnerConfig = await props.openCustomConfig(); + } + + const activateCustomConfig = async () => { + const customConfig: customScriptRunnerConfig = { baseConfiguration: baseConfig, dependencies }; + setLoading(true); + try { + await props.activateCustomScriptRunner(customConfig); + } catch (e) { + console.log(e) + } finally { + setLoading(false); + } + } + + const onSelectBaseConfig = (e: React.ChangeEvent) => { + setBaseConfig(e.target.value); + } + + const toggleRequire = () => { + setUseRequire((prev) => !prev) + } + + if (loading) { + return
+
+ +
+
+ } + + return ( +
+
Custom configuration
+ + + +
+ setName(e.target.value)} + style={{ marginRight: '10px' }} + /> + setAlias(e.target.value)} /> + setVersion(e.target.value)} + /> + +
+ + +
+
+ +
+
    + {dependencies.map((dependency, index) => ( +
  • +
    + {dependency.name} - {dependency.version} + +
    +
  • + ))} +
+ {dependencies.length > 0 && ( + + )} + + {dependencies.length > 0 && ( + )} +
+ ); +} \ No newline at end of file diff --git a/libs/remix-ui/scriptrunner/src/lib/script-runner-ui.tsx b/libs/remix-ui/scriptrunner/src/lib/script-runner-ui.tsx new file mode 100644 index 0000000000..52692be16f --- /dev/null +++ b/libs/remix-ui/scriptrunner/src/lib/script-runner-ui.tsx @@ -0,0 +1,103 @@ +import React, { useEffect, useState } from "react"; +import { Accordion, Button } from "react-bootstrap"; +import { customScriptRunnerConfig, ProjectConfiguration } from "../types"; +import { faCaretDown, faCaretRight, faCheck, faExclamationCircle, faRedoAlt, faToggleOn } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { CustomScriptRunner } from "./custom-script-runner"; +import { CustomTooltip } from "@remix-ui/helper"; + +export interface ScriptRunnerUIProps { + loadScriptRunner: (config: ProjectConfiguration) => void; + openCustomConfig: () => any; + saveCustomConfig(content: customScriptRunnerConfig): void; + activateCustomScriptRunner(config: customScriptRunnerConfig): Promise; + customConfig: customScriptRunnerConfig; + configurations: ProjectConfiguration[]; + activeConfig: ProjectConfiguration; + enableCustomScriptRunner: boolean; +} + +export const ScriptRunnerUI = (props: ScriptRunnerUIProps) => { + const { loadScriptRunner, configurations, activeConfig, enableCustomScriptRunner } = props; + const [activeKey, setActiveKey] = useState('default'); + + useEffect(() => { + if (activeConfig) { + setActiveKey(activeConfig.name) + } + },[activeConfig]) + + if (!configurations) { + return
Loading...
; + } + + return ( +
+ + {configurations.filter((config) => config.publish).map((config: ProjectConfiguration, index) => ( +
+
+ setActiveKey(activeKey === config.name ? '' : config.name)} + > +
+ {activeKey === config.name ? + : + } +
{config.title || config.name}
+
+
+
+ {config.isLoading &&
+ +
} + {config.errorStatus && config.error &&
+ + + + +
} + {!config.isLoading && config.errorStatus && config.error && +
loadScriptRunner(config)} className="pointer px-2"> + +
} + {!config.isLoading && !config.errorStatus && !config.error && +
loadScriptRunner(config)} className="pointer px-2"> + {activeConfig && activeConfig.name !== config.name ? + : + + } +
+ } +
+
+ + + <> +

Description: {config.description}

+

Dependencies:

+
    + {config.dependencies.map((dep, depIndex) => ( +
  • + {dep.name} (v{dep.version}) +
  • + ))} +
+
))} +
+ {enableCustomScriptRunner && + config.publish)} + />} +
+ ); +}; + diff --git a/libs/remix-ui/scriptrunner/src/types/index.ts b/libs/remix-ui/scriptrunner/src/types/index.ts new file mode 100644 index 0000000000..d675fa2084 --- /dev/null +++ b/libs/remix-ui/scriptrunner/src/types/index.ts @@ -0,0 +1,37 @@ +import { defaultConfig } from "@web3modal/ethers5/react"; + +export interface Dependency { + version: string; + name: string; + alias?: string; + import?: boolean; + require: boolean; + windowImport?: boolean; + } + +export interface Replacements { + [key: string]: string; + } + +export interface ProjectConfiguration { + name: string; + publish: boolean; + description: string; + dependencies: Dependency[]; + replacements: Replacements; + title: string; + errorStatus: boolean; + error: string; + isLoading: boolean; + } + +export interface customScriptRunnerConfig { + baseConfiguration: string; + dependencies: Dependency[]; + } + +export interface ScriptRunnerConfig { + defaultConfig: string, + customConfig: customScriptRunnerConfig + } + diff --git a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx index 05596f3ee6..bae6a258c2 100644 --- a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx +++ b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx @@ -1,7 +1,7 @@ import { fileDecoration, FileDecorationIcons } from '@remix-ui/file-decorators' import { CustomTooltip } from '@remix-ui/helper' import { Plugin } from '@remixproject/engine' -import React, {useState, useRef, useEffect, useReducer} from 'react' // eslint-disable-line +import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line import { FormattedMessage } from 'react-intl' import { Tab, Tabs, TabList, TabPanel } from 'react-tabs' import './remix-ui-tabs.css' @@ -86,10 +86,10 @@ export const TabsUI = (props: TabsUIProps) => { } }, [tabsState.selectedIndex]) - const getAI = async() => { + const getAI = async () => { try { return await props.plugin.call('settings', 'getCopilotSetting') - } catch (e){ + } catch (e) { return false } } @@ -208,7 +208,7 @@ export const TabsUI = (props: TabsUIProps) => { const path = active().substr(active().indexOf('/') + 1, active().length) const content = await props.plugin.call('fileManager', 'readFile', path) if (tabsState.currentExt === 'js' || tabsState.currentExt === 'ts') { - await props.plugin.call('scriptRunner', 'execute', content, path) + await props.plugin.call('scriptRunnerBridge', 'execute', content, path) _paq.push(['trackEvent', 'editor', 'clickRunFromEditor', tabsState.currentExt]) } else if (tabsState.currentExt === 'sol' || tabsState.currentExt === 'yul') { await props.plugin.call('solidity', 'compile', path) @@ -225,14 +225,32 @@ export const TabsUI = (props: TabsUIProps) => { + {(tabsState.currentExt === 'ts' || tabsState.currentExt === 'js') -
+ && + + + }> + } +
- {tabsState.currentExt === 'sol'? ( + {tabsState.currentExt === 'sol' ? ( ) : ( @@ -289,7 +307,7 @@ export const TabsUI = (props: TabsUIProps) => { tooltipId="overlay-tooltip-copilot" tooltipText={ - { tabsState.currentExt === 'sol'? ( + {tabsState.currentExt === 'sol' ? ( !ai_switch ? ( ) : () @@ -303,7 +321,7 @@ export const TabsUI = (props: TabsUIProps) => { data-id="remix_ai_switch" id='remix_ai_switch' className="btn ai-switch text-ai pl-2 pr-0 py-0" - disabled={!(tabsState.currentExt === 'sol' )} + disabled={!(tabsState.currentExt === 'sol')} onClick={async () => { await props.plugin.call('settings', 'updateCopilotChoice', !ai_switch) setAI_switch(!ai_switch) @@ -315,7 +333,7 @@ export const TabsUI = (props: TabsUIProps) => {
-
+
}> props.onZoomOut()}> diff --git a/libs/remix-ui/terminal/src/lib/actions/terminalAction.ts b/libs/remix-ui/terminal/src/lib/actions/terminalAction.ts index ceeea496f7..9d9abbb760 100644 --- a/libs/remix-ui/terminal/src/lib/actions/terminalAction.ts +++ b/libs/remix-ui/terminal/src/lib/actions/terminalAction.ts @@ -79,28 +79,28 @@ export const filterFnAction = (name: string, filterFn, dispatch: React.Dispatch< } export const registerLogScriptRunnerAction = (on, commandName, commandFn, dispatch: React.Dispatch) => { - on('scriptRunner', commandName, (msg) => { + on('scriptRunnerBridge', commandName, (msg) => { commandFn.log.apply(commandFn, msg.data) // eslint-disable-line dispatch({ type: commandName, payload: { commandFn, message: msg.data } }) }) } export const registerInfoScriptRunnerAction = (on, commandName, commandFn, dispatch: React.Dispatch) => { - on('scriptRunner', commandName, (msg) => { + on('scriptRunnerBridge', commandName, (msg) => { commandFn.info.apply(commandFn, msg.data) // eslint-disable-line dispatch({ type: commandName, payload: { commandFn, message: msg.data } }) }) } export const registerWarnScriptRunnerAction = (on, commandName, commandFn, dispatch: React.Dispatch) => { - on('scriptRunner', commandName, (msg) => { + on('scriptRunnerBridge', commandName, (msg) => { commandFn.warn.apply(commandFn, msg.data) // eslint-disable-line dispatch({ type: commandName, payload: { commandFn, message: msg.data } }) }) } export const registerErrorScriptRunnerAction = (on, commandName, commandFn, dispatch: React.Dispatch) => { - on('scriptRunner', commandName, (msg) => { + on('scriptRunnerBridge', commandName, (msg) => { commandFn.error.apply(commandFn, msg.data) // eslint-disable-line dispatch({ type: commandName, payload: { commandFn, message: msg.data } }) }) diff --git a/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx b/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx index e7a4feb0aa..4e0d224c1b 100644 --- a/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx +++ b/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx @@ -245,7 +245,7 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => { await call('remixAI', 'solidity_answer', script) // No streaming supported in terminal _paq.push(['trackEvent', 'ai', 'remixAI', 'askFromTerminal']) } else { - await call('scriptRunner', 'execute', script) + await call('scriptRunnerBridge', 'execute', script) } done() } catch (error) { diff --git a/libs/remix-ui/workspace/src/lib/actions/index.tsx b/libs/remix-ui/workspace/src/lib/actions/index.tsx index cc66374596..7385b1316e 100644 --- a/libs/remix-ui/workspace/src/lib/actions/index.tsx +++ b/libs/remix-ui/workspace/src/lib/actions/index.tsx @@ -505,7 +505,7 @@ export const runScript = async (path: string) => { if (error) { return dispatch(displayPopUp(error)) } - plugin.call('scriptRunner', 'execute', content, path) + plugin.call('scriptRunnerBridge', 'execute', content, path) }) } @@ -530,7 +530,7 @@ export const signTypedData = async (path: string) => { plugin.call('terminal', 'log', { type: 'log', value: `${path} signature using ${settings.selectedAccount} : ${result}` }) } catch (e) { console.error(e) - plugin.call('terminal', 'log', { type: 'error', value: `error while signing ${path}: ${e.message}` }) + plugin.call('terminal', 'log', { type: 'error', value: `error while signing ${path}: ${e.message || e}` }) dispatch(displayPopUp(e.message)) } } diff --git a/tsconfig.paths.json b/tsconfig.paths.json index d373a783b1..a61989661d 100644 --- a/tsconfig.paths.json +++ b/tsconfig.paths.json @@ -188,6 +188,9 @@ "@remix-api": [ "libs/remix-api/src/index.ts" ], + "@remix-scriptrunner": [ + "libs/remix-ui/scriptrunner/src/index.ts" + ], "@remix-git": [ "libs/remix-git/" ]