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

pull/5371/head
bunsenstraat 2 weeks ago
commit 86b012f5bd
  1. 38
      .circleci/config.yml
  2. 2
      .github/workflows/pr-reminder.yml
  3. 1
      apps/contract-verification/src/app/components/NavMenu.tsx
  4. 28
      apps/contract-verification/src/app/components/SearchableChainDropdown.tsx
  5. 2
      apps/contract-verification/src/app/layouts/Default.tsx
  6. 2
      apps/contract-verification/src/app/views/ReceiptsView.tsx
  7. 6
      apps/contract-verification/src/app/views/VerifyView.tsx
  8. 2
      apps/remix-dapp/src/utils/txRunner.ts
  9. 2
      apps/remix-ide-e2e/nightwatch-chrome.ts
  10. 35
      apps/remix-ide-e2e/src/commands/hideMetaMaskPopup.ts
  11. 36
      apps/remix-ide-e2e/src/commands/setupMetamask.ts
  12. 38
      apps/remix-ide-e2e/src/tests/contract_verification.test.ts
  13. 2
      apps/remix-ide-e2e/src/tests/matomo.test.ts
  14. 433
      apps/remix-ide-e2e/src/tests/metamask.test.ts
  15. 123
      apps/remix-ide-e2e/src/tests/providers.test.ts
  16. 116
      apps/remix-ide-e2e/src/tests/script-runner.test.ts
  17. 2
      apps/remix-ide/ci/browser_test.sh
  18. 33
      apps/remix-ide/ci/metamask.sh
  19. 7
      apps/remix-ide/src/app.js
  20. 6
      apps/remix-ide/src/app/components/hidden-panel.tsx
  21. 8
      apps/remix-ide/src/app/panels/terminal.tsx
  22. 2
      apps/remix-ide/src/app/plugins/matomo.ts
  23. 4
      apps/remix-ide/src/app/plugins/parser/services/code-parser-compiler.ts
  24. 38
      apps/remix-ide/src/app/providers/abstract-provider.tsx
  25. 2
      apps/remix-ide/src/app/tabs/compile-and-run.ts
  26. 1
      apps/remix-ide/src/app/tabs/locales/en/remixUiTabs.json
  27. 399
      apps/remix-ide/src/app/tabs/script-runner-ui.tsx
  28. 2
      apps/remix-ide/src/app/tabs/web3-provider.js
  29. 57
      apps/remix-ide/src/app/udapp/run-tab.tsx
  30. 446
      apps/remix-ide/src/assets/fontawesome/css/all.css
  31. BIN
      apps/remix-ide/src/assets/fontawesome/webfonts/custom-icons.ttf
  32. BIN
      apps/remix-ide/src/assets/fontawesome/webfonts/custom-icons.woff2
  33. 1
      apps/remix-ide/src/assets/img/solid-gear-circle-play.svg
  34. 10
      apps/remix-ide/src/blockchain/blockchain.tsx
  35. 12
      apps/remix-ide/src/remixAppManager.js
  36. 1
      apps/remix-ide/src/remixEngine.js
  37. 2
      apps/remix-ide/team-best-practices.md
  38. 2
      apps/remixdesktop/test/nighwatch.app.ts
  39. 1
      libs/remix-api/src/lib/plugins/fileSystem-api.ts
  40. 14
      libs/remix-api/src/lib/plugins/menuicons-api.ts
  41. 2
      libs/remix-api/src/lib/remix-api.ts
  42. 6
      libs/remix-lib/src/execution/txRunnerWeb3.ts
  43. 5
      libs/remix-simulator/src/methods/accounts.ts
  44. 10
      libs/remix-simulator/src/methods/transactions.ts
  45. 7
      libs/remix-ui/editor/src/lib/remix-ui-editor.tsx
  46. 2
      libs/remix-ui/home-tab/src/lib/components/homeTabTitle.tsx
  47. 11
      libs/remix-ui/run-tab/src/lib/actions/deploy.ts
  48. 2
      libs/remix-ui/scriptrunner/src/index.ts
  49. 168
      libs/remix-ui/scriptrunner/src/lib/custom-script-runner.tsx
  50. 103
      libs/remix-ui/scriptrunner/src/lib/script-runner-ui.tsx
  51. 37
      libs/remix-ui/scriptrunner/src/types/index.ts
  52. 36
      libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx
  53. 8
      libs/remix-ui/terminal/src/lib/actions/terminalAction.ts
  54. 2
      libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx
  55. 4
      libs/remix-ui/workspace/src/lib/actions/index.tsx
  56. 3
      tsconfig.paths.json

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

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

@ -10,6 +10,7 @@ interface NavItemProps {
const NavItem: React.FC<NavItemProps> = ({ to, icon, title }) => {
return (
<NavLink
data-id={`${title}Tab`}
to={to}
className={({ isActive }) => '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")}
>

@ -37,7 +37,6 @@ export const SearchableChainDropdown: React.FC<DropdownProps> = ({ label, id, se
const [searchTerm, setSearchTerm] = useState(selectedChain ? getChainDescriptor(selectedChain) : '')
const [isOpen, setIsOpen] = useState(false)
const [filteredOptions, setFilteredOptions] = useState<Chain[]>(dropdownChains)
const dropdownRef = useRef<HTMLDivElement>(null)
const fuse = new Fuse(dropdownChains, {
@ -45,14 +44,7 @@ export const SearchableChainDropdown: React.FC<DropdownProps> = ({ 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<DropdownProps> = ({ label, id, se
{' '}
{/* Add ref here */}
<label htmlFor={id}>{label}</label>
<input type="text" value={searchTerm} onChange={handleInputChange} onClick={openDropdown} placeholder="Select a chain" className="form-control" />
{isOpen && (
<ul className="dropdown-menu show w-100 bg-light" style={{ maxHeight: '400px', overflowY: 'auto' }}>
{filteredOptions.map((chain) => (
<li key={chain.chainId} onClick={() => handleOptionClick(chain)} className={`dropdown-item text-dark ${selectedChain?.chainId === chain.chainId ? 'active' : ''}`} style={{ cursor: 'pointer', whiteSpace: 'normal' }}>
{getChainDescriptor(chain)}
</li>
))}
</ul>
)}
<input type="text" value={searchTerm} onChange={handleInputChange} onClick={openDropdown} data-id="chainDropdownbox" placeholder="Select a chain" className="form-control" />
<ul className="dropdown-menu show w-100 bg-light" style={{ maxHeight: '400px', overflowY: 'auto', display: isOpen ? 'initial' : 'none' }}>
{filteredOptions.map((chain) => (
<li key={chain.chainId} onClick={() => handleOptionClick(chain)} data-id={chain.chainId} className={`dropdown-item text-dark ${selectedChain?.chainId === chain.chainId ? 'active' : ''}`} style={{ cursor: 'pointer', whiteSpace: 'normal' }}>
{getChainDescriptor(chain)}
</li>
))}
</ul>
</div>
)
}

@ -13,7 +13,7 @@ export const DefaultLayout = ({ children, title, description }: PropsWithChildre
<div className="d-flex flex-column h-100">
<NavMenu />
<div className="py-4 px-3 flex-grow-1 bg-light" style={{ overflowY: 'auto' }}>
<div>
<div data-id={`${title}Description`}>
<p className="text-center" style={{ fontSize: '0.8rem' }}>
{description}
</p>

@ -10,7 +10,7 @@ export const ReceiptsView = () => {
<div>
{contracts.length > 0 ? contracts.map((contract, index) => (
<AccordionReceipt contract={contract} index={index} />
)) : <div className="text-center mt-5">No contracts submitted for verification</div>}
)) : <div className="text-center mt-5" data-id="noContractsSubmitted">No contracts submitted for verification</div>}
</div>
)
}

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

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

@ -21,7 +21,7 @@ module.exports = {
'default': {
globals: {
waitForConditionTimeout: 10000,
asyncHookTimeout: 10000000
asyncHookTimeout: 120000
},
screenshots: {
enabled: true,

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

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

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

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

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

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

@ -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<any>} 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<any>, accountIndex?: number): Promise<ethers.Contract> => {
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)
}
})()`

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

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

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

@ -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 <RemixPluginPanel header={<></>} plugins={state.plugins} />
}

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

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

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

@ -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<JsonDataResult>
}
@ -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<JsonDataResult> {
// 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' : []

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

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

@ -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<any> = () => { }
workspaceScriptRunnerDefaults: Record<string, string>
customConfig: ScriptRunnerConfig
configurations: ProjectConfiguration[]
activeConfig: ProjectConfiguration
enableCustomScriptRunner: boolean
plugin: Plugin<any, CustomRemixApi>
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 (
<div id="scriptrunnerTab">
<PluginViewWrapper plugin={this} />
</div>
)
}
setDispatch(dispatch: React.Dispatch<any>) {
this.dispatch = dispatch
this.renderComponent()
}
renderComponent() {
this.dispatch({
customConfig: this.customConfig,
configurations: this.configurations,
activeConfig: this.activeConfig,
enableCustomScriptRunner: this.enableCustomScriptRunner
})
}
updateComponent(state: IScriptRunnerState) {
return (
<ScriptRunnerUI
customConfig={state.customConfig}
configurations={state.configurations}
activeConfig={state.activeConfig}
enableCustomScriptRunner={state.enableCustomScriptRunner}
activateCustomScriptRunner={this.activateCustomScriptRunner.bind(this)}
saveCustomConfig={this.saveCustomConfig.bind(this)}
openCustomConfig={this.openCustomConfig.bind(this)}
loadScriptRunner={this.selectScriptRunner.bind(this)} />
)
}
async selectScriptRunner(config: ProjectConfiguration) {
if (await this.loadScriptRunner(config))
await this.saveCustomConfig(this.customConfig)
}
async loadScriptRunner(config: ProjectConfiguration): Promise<boolean> {
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}&timestamp=${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}&timestamp=${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<void> {
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)
}
}
}
}

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

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

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

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc.--><path d="M16 166.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3l-.1 .1c3.2 8.7 .5 18.4-6.4 24.6l-1.1 1C394.7 196.1 320 273.3 320 368c0 22.7 4.3 44.3 12.1 64.2c-1 .4-2 .9-3 1.3l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6zM186.7 216c-14.3 24.8-14.3 55.2 0 80s40.7 40 69.3 40s55-15.2 69.3-40s14.3-55.2 0-80s-40.7-40-69.3-40s-55 15.2-69.3 40zM352 368c0-51.4 27.4-99 72-124.7s99.4-25.7 144 0s72 73.3 72 124.7s-27.4 99-72 124.7s-99.4 25.7-144 0S352 419.4 352 368zm96-48l0 96c0 5.8 3.1 11.1 8.1 13.9s11.2 2.8 16.1-.2l80-48c4.8-2.9 7.8-8.1 7.8-13.7s-2.9-10.8-7.8-13.7l-80-48c-4.9-3-11.1-3-16.1-.2s-8.1 8.2-8.1 13.9z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

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

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

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

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

@ -42,7 +42,7 @@ module.exports = {
selenium_host: 'localhost',
globals: {
waitForConditionTimeout: 10000,
asyncHookTimeout: 100000
asyncHookTimeout: 30000
},
screenshots: {
enabled: true,

@ -9,5 +9,6 @@ export interface IExtendedFileSystem extends IFileSystem {
refresh(): Promise<void>
hasGitSubmodules(): Promise<boolean>
isGitRepo(): Promise<boolean>
exists(file: string): Promise<boolean>
};
}

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

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

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

@ -76,6 +76,7 @@ export class Web3Accounts {
methods (): Record<string, unknown> {
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))
}

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

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

@ -46,7 +46,7 @@ const iconButtons: HometabIconSection[] = [
{
textToolip: <FormattedMessage id="home.joinUsOnDiscord" />,
matomoTrackingEntry: ['trackEvent', 'hometab', 'socialmedia', 'discord'],
urlLink: 'https://discord.gg/mh9hFCKkEq',
urlLink: 'https://discord.com/invite/nfv6ZYjAeP',
iconClass: 'fa-discord',
placement: 'top'
}

@ -119,10 +119,19 @@ const getConfirmationCb = (plugin: RunTab, dispatch: React.Dispatch<any>, confir
export const continueHandler = (dispatch: React.Dispatch<any>, 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.'

@ -0,0 +1,2 @@
export { ScriptRunnerUI } from './lib/script-runner-ui';
export * from './types';

@ -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<string>;
customConfig: customScriptRunnerConfig;
}
export const CustomScriptRunner = (props: ScriptRunnerUIProps) => {
const [dependencies, setDependencies] = useState<Dependency[]>([]);
const [name, setName] = useState<string>('');
const [alias, setAlias] = useState<string>('');
const [version, setVersion] = useState<string>('');
const [baseConfig, setBaseConfig] = useState<string>('default');
const [loading, setLoading] = useState<boolean>(false);
const [useRequire, setUseRequire] = useState<boolean>(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<HTMLSelectElement>) => {
setBaseConfig(e.target.value);
}
const toggleRequire = () => {
setUseRequire((prev) => !prev)
}
if (loading) {
return <div style={{ padding: '20px', maxWidth: '400px', margin: 'auto' }}>
<div className="text-center py-5">
<i className="fas fa-spinner fa-pulse fa-2x"></i>
</div>
</div>
}
return (
<div style={{ padding: '20px', maxWidth: '400px', margin: 'auto' }}>
<h5>Custom configuration</h5>
<label>Select a base configuration</label>
<select value={baseConfig} className="form-control" onChange={onSelectBaseConfig} style={{ marginBottom: '10px' }}>
<option value="none">Select a base configuration</option>
{props.publishedConfigurations.map((config: ProjectConfiguration, index) => (
<option key={index} value={config.name}>
{config.name}
</option>
))}
</select>
<label>Add dependencies</label>
<div style={{ marginBottom: '10px' }}>
<input
type="text"
placeholder="Dependency Name"
value={name}
className="form-control"
onChange={(e) => setName(e.target.value)}
style={{ marginRight: '10px' }}
/>
<input
type="text"
placeholder="Alias"
className="form-control mt-1"
value={alias}
onChange={(e) => setAlias(e.target.value)} />
<input
type="text"
placeholder="Version"
className="form-control mt-1"
value={version}
onChange={(e) => setVersion(e.target.value)}
/>
<CustomTooltip
placement="bottom"
tooltipText="use require when the module doesn't support import statements"
>
<div>
<label className="pr-2 pt-2">Use 'require':</label>
<FontAwesomeIcon className={useRequire ? 'text-success' : ''} onClick={toggleRequire} icon={useRequire ? faToggleOn : faToggleOff}></FontAwesomeIcon>
</div>
</CustomTooltip>
<button
className="btn btn-primary w-100 mt-1"
onClick={handleAddDependency}>
Add
</button>
</div>
<ul>
{dependencies.map((dependency, index) => (
<li key={index} style={{ marginBottom: '5px' }}>
<div className="d-flex align-items-baseline justify-content-between">
{dependency.name} - {dependency.version}
<button
onClick={() => handleRemoveDependency(index)}
className="btn btn-danger"
style={{ marginLeft: '10px' }}
>
<FontAwesomeIcon icon={faTrash} />
</button>
</div>
</li>
))}
</ul>
{dependencies.length > 0 && (
<button className="btn btn-primary w-100" onClick={handleSaveToFile} style={{ marginTop: '20px' }}>
Save config
</button>
)}
<button className="btn btn-primary w-100" onClick={openConfig} style={{ marginTop: '20px' }}>
Open config
</button>
{dependencies.length > 0 && (
<button className="btn btn-success w-100" onClick={activateCustomConfig} style={{ marginTop: '20px' }}>
Activate
</button>)}
</div>
);
}

@ -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<string>;
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 <div>Loading...</div>;
}
return (
<div className="px-1">
<Accordion activeKey={activeKey} defaultActiveKey="default">
{configurations.filter((config) => config.publish).map((config: ProjectConfiguration, index) => (
<div key={index}>
<div className="d-flex align-items-baseline justify-content-between">
<Accordion.Toggle as={Button} variant="link" eventKey={config.name}
style={{
overflowX: 'hidden',
textOverflow: 'ellipsis'
}}
onClick={() => setActiveKey(activeKey === config.name ? '' : config.name)}
>
<div className="d-flex">
{activeKey === config.name ?
<FontAwesomeIcon icon={faCaretDown}></FontAwesomeIcon> :
<FontAwesomeIcon icon={faCaretRight}></FontAwesomeIcon>}
<div data-id={`sr-list-${config.name}`} className="pl-2">{config.title || config.name}</div>
</div>
</Accordion.Toggle>
<div className="d-flex align-items-baseline">
{config.isLoading && <div className="">
<i className="fas fa-spinner fa-spin mr-1"></i>
</div>}
{config.errorStatus && config.error && <div className="text-danger">
<CustomTooltip tooltipText={config.error}>
<FontAwesomeIcon data-id={`sr-error-${config.name}`} icon={faExclamationCircle}></FontAwesomeIcon>
</CustomTooltip>
</div>}
{!config.isLoading && config.errorStatus && config.error &&
<div onClick={() => loadScriptRunner(config)} className="pointer px-2">
<FontAwesomeIcon data-id={`sr-reload-${config.name}`} icon={faRedoAlt}></FontAwesomeIcon>
</div>}
{!config.isLoading && !config.errorStatus && !config.error &&
<div onClick={() => loadScriptRunner(config)} className="pointer px-2">
{activeConfig && activeConfig.name !== config.name ?
<FontAwesomeIcon data-id={`sr-toggle-${config.name}`} icon={faToggleOn}></FontAwesomeIcon> :
<FontAwesomeIcon data-id={`sr-loaded-${config.name}`} className="text-success" icon={faCheck}></FontAwesomeIcon>
}
</div>
}
</div>
</div>
<Accordion.Collapse className="px-4" eventKey={config.name}>
<>
<p><strong>Description: </strong>{config.description}</p>
<p><strong>Dependencies:</strong></p>
<ul>
{config.dependencies.map((dep, depIndex) => (
<li data-id={`dependency-${dep.name}-${dep.version}`} key={depIndex}>
<strong>{dep.name}</strong> (v{dep.version})
</li>
))}
</ul></>
</Accordion.Collapse></div>))}
</Accordion>
{enableCustomScriptRunner &&
<CustomScriptRunner
customConfig={props.customConfig}
activateCustomScriptRunner={props.activateCustomScriptRunner}
saveCustomConfig={props.saveCustomConfig}
openCustomConfig={props.openCustomConfig}
publishedConfigurations={configurations.filter((config) => config.publish)}
/>}
</div>
);
};

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

@ -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) => {
<i className="fas fa-play"></i>
</button>
</CustomTooltip>
{(tabsState.currentExt === 'ts' || tabsState.currentExt === 'js')
<div className= "d-flex border-left ml-2 align-items-center" style={{ height: "3em" }}>
&& <CustomTooltip
placement="bottom"
tooltipId="overlay-tooltip-run-script-config"
tooltipText={
<span>
<FormattedMessage id="remixUiTabs.tooltipText9" />
</span>
}><button
data-id="script-config"
className="btn text-dark border-left ml-2 pr-0 py-0 d-flex"
onClick={async () => {
props.plugin.call('menuicons', 'select', 'scriptRunnerBridge')
}}
>
<i className="fa-kit fa-solid-gear-circle-play"></i>
</button></CustomTooltip>
}
<div className="d-flex border-left ml-2 align-items-center" style={{ height: "3em" }}>
<CustomTooltip
placement="bottom"
tooltipId="overlay-tooltip-explaination"
tooltipText={
<span>
{tabsState.currentExt === 'sol'? (
{tabsState.currentExt === 'sol' ? (
<FormattedMessage id="remixUiTabs.tooltipText5" />
) : (
<FormattedMessage id="remixUiTabs.tooltipText4" />
@ -289,7 +307,7 @@ export const TabsUI = (props: TabsUIProps) => {
tooltipId="overlay-tooltip-copilot"
tooltipText={
<span>
{ tabsState.currentExt === 'sol'? (
{tabsState.currentExt === 'sol' ? (
!ai_switch ? (
<FormattedMessage id="remixUiTabs.tooltipText6" />
) : (<FormattedMessage id="remixUiTabs.tooltipText7" />)
@ -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) => {
</CustomTooltip>
</div>
<div className= "d-flex border-left ml-2 align-items-center" style={{ height: "3em" }}>
<div className="d-flex border-left ml-2 align-items-center" style={{ height: "3em" }}>
<CustomTooltip placement="bottom" tooltipId="overlay-tooltip-zoom-out" tooltipText={<FormattedMessage id="remixUiTabs.zoomOut" />}>
<span data-id="tabProxyZoomOut" className="btn fas fa-search-minus text-dark pl-2 pr-0 py-0 d-flex" onClick={() => props.onZoomOut()}></span>
</CustomTooltip>

@ -79,28 +79,28 @@ export const filterFnAction = (name: string, filterFn, dispatch: React.Dispatch<
}
export const registerLogScriptRunnerAction = (on, commandName, commandFn, dispatch: React.Dispatch<any>) => {
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<any>) => {
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<any>) => {
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<any>) => {
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 } })
})

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

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

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

Loading…
Cancel
Save