remix-project mirror
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
remix-project/apps/remix-ide/src/remixAppManager.js

363 lines
9.6 KiB

import {PluginManager} from '@remixproject/engine'
import {EventEmitter} from 'events'
import {QueryParams} from '@remix-project/remix-lib'
import {IframePlugin} from '@remixproject/engine-web'
const _paq = (window._paq = window._paq || [])
// requiredModule removes the plugin from the plugin manager list on UI
const requiredModules = [
// services + layout views + system views
'manager',
'config',
'compilerArtefacts',
'compilerMetadata',
'contextualListener',
'editor',
'offsetToLineColumnConverter',
'network',
'theme',
'locale',
'fileManager',
'contentImport',
'blockchain',
'web3Provider',
'scriptRunner',
'fetchAndCompile',
'mainPanel',
'hiddenPanel',
'sidePanel',
'menuicons',
'filePanel',
'terminal',
'settings',
'pluginManager',
'tabs',
'udapp',
'dGitProvider',
'solidity',
'solidity-logic',
'gistHandler',
'layout',
'notification',
'permissionhandler',
'walkthrough',
'storage',
'restorebackupzip',
'link-libraries',
'deploy-libraries',
'openzeppelin-proxy',
'hardhat-provider',
'ganache-provider',
'foundry-provider',
'basic-http-provider',
'injected',
'injected-trustwallet',
'injected-optimism-provider',
'injected-arbitrum-one-provider',
'injected-ephemery-testnet-provider',
'injected-skale-chaos-testnet-provider',
'vm-custom-fork',
'vm-goerli-fork',
'vm-mainnet-fork',
'vm-sepolia-fork',
'vm-merge',
'vm-london',
'vm-berlin',
2 years ago
'vm-shanghai',
'compileAndRun',
'search',
'recorder',
'fileDecorator',
'codeParser',
'codeFormatter',
'solidityumlgen',
'contractflattener',
'solidity-script'
]
// dependentModules shouldn't be manually activated (e.g hardhat is activated by remixd)
const dependentModules = ['foundry', 'hardhat', 'truffle', 'slither']
const loadLocalPlugins = ['doc-gen', 'doc-viewer', 'etherscan', 'vyper', 'solhint', 'walletconnect', 'circuit-compiler']
3 years ago
const sensitiveCalls = {
fileManager: ['writeFile', 'copyFile', 'rename', 'copyDir'],
contentImport: ['resolveAndSave'],
web3Provider: ['sendAsync']
3 years ago
}
export function isNative(name) {
// nativePlugin allows to bypass the permission request
const nativePlugins = [
'vyper',
'workshops',
'debugger',
'remixd',
'menuicons',
'solidity',
'solidity-logic',
'solidityStaticAnalysis',
'solidityUnitTesting',
'layout',
'notification',
'hardhat-provider',
'ganache-provider',
'foundry-provider',
'basic-http-provider',
'injected-optimism-provider',
'tabs',
'injected-arbitrum-one-provider',
'injected-skale-chaos-testnet-provider',
'injected-ephemery-testnet-provider',
'injected',
'doc-gen',
'doc-viewer'
]
return nativePlugins.includes(name) || requiredModules.includes(name)
}
/**
* Checks if plugin caller 'from' is allowed to activate plugin 'to'
* The caller can have 'canActivate' as a optional property in the plugin profile.
* This is an array containing the 'name' property of the plugin it wants to call.
* canActivate = ['plugin1-to-call','plugin2-to-call',....]
* or the plugin is allowed by default because it is native
*
* @param {any, any}
* @returns {boolean}
*/
3 years ago
export function canActivate(from, to) {
return ['ethdoc'].includes(from.name) || isNative(from.name) || (to && from && from.canActivate && from.canActivate.includes(to.name))
}
5 years ago
export class RemixAppManager extends PluginManager {
3 years ago
constructor() {
5 years ago
super()
this.event = new EventEmitter()
this.pluginsDirectory = 'https://raw.githubusercontent.com/ethereum/remix-plugins-directory/master/build/metadata.json'
this.pluginLoader = new PluginLoader()
}
3 years ago
async canActivatePlugin(from, to) {
return canActivate(from, to)
5 years ago
}
3 years ago
async canDeactivatePlugin(from, to) {
if (requiredModules.includes(to.name)) return false
return isNative(from.name)
5 years ago
}
3 years ago
async canDeactivate(from, to) {
return this.canDeactivatePlugin(from, to)
3 years ago
}
3 years ago
async deactivatePlugin(name) {
const profile = await this.getProfile(name)
const [to, from] = [profile, await this.getProfile(this.requestFrom)]
if (this.canDeactivatePlugin(from, to)) {
if (profile.methods.includes('deactivate')) {
try {
await this.call(name, 'deactivate')
} catch (e) {
console.log(e)
}
}
await this.toggleActive(name)
}
}
3 years ago
async canCall(from, to, method, message) {
const isSensitiveCall = sensitiveCalls[to] && sensitiveCalls[to].includes(method)
5 years ago
// Make sure the caller of this methods is the target plugin
if (to !== this.currentRequest.from) {
5 years ago
return false
}
// skipping native plugins' requests
3 years ago
if (isNative(from)) {
return true
}
3 years ago
// ask the user for permission
3 years ago
return await this.call('permissionhandler', 'askPermission', this.profiles[from], this.profiles[to], method, message, isSensitiveCall)
5 years ago
}
3 years ago
onPluginActivated(plugin) {
this.pluginLoader.set(
plugin,
this.actives.filter((plugin) => !this.isDependent(plugin))
)
this.event.emit('activate', plugin)
this.emit('activate', plugin)
if (!requiredModules.includes(plugin.name)) _paq.push(['trackEvent', 'pluginManager', 'activate', plugin.name])
}
3 years ago
getAll() {
return Object.keys(this.profiles).map((p) => {
return this.profiles[p]
})
}
3 years ago
getIds() {
return Object.keys(this.profiles)
}
3 years ago
onPluginDeactivated(plugin) {
this.pluginLoader.set(
plugin,
this.actives.filter((plugin) => !this.isDependent(plugin))
)
this.event.emit('deactivate', plugin)
_paq.push(['trackEvent', 'pluginManager', 'deactivate', plugin.name])
}
3 years ago
isDependent(name) {
return dependentModules.includes(name)
}
3 years ago
isRequired(name) {
// excluding internal use plugins
return requiredModules.includes(name)
}
3 years ago
async registeredPlugins() {
let plugins
try {
const res = await fetch(this.pluginsDirectory)
plugins = await res.json()
4 years ago
plugins = plugins.filter((plugin) => {
if (plugin.targets && Array.isArray(plugin.targets) && plugin.targets.length > 0) {
return plugin.targets.includes('remix')
4 years ago
}
return true
})
localStorage.setItem('plugins-directory', JSON.stringify(plugins))
} catch (e) {
console.log('getting plugins list from localstorage...')
const savedPlugins = localStorage.getItem('plugins-directory')
if (savedPlugins) {
try {
4 years ago
plugins = JSON.parse(savedPlugins)
} catch (e) {
console.error(e)
}
}
}
2 years ago
const testPluginName = localStorage.getItem('test-plugin-name')
const testPluginUrl = localStorage.getItem('test-plugin-url')
for (let plugin of loadLocalPlugins) {
// fetch the profile from the local plugin
try {
const profile = await fetch(`plugins/${plugin}/profile.json`)
const profileJson = await profile.json()
// remove duplicates
plugins = plugins.filter((p) => p.name !== profileJson.name && p.displayName !== profileJson.displayName)
// change url
profileJson.url = `plugins/${plugin}/index.html`
// add the local plugin
plugins.push(profileJson)
} catch (e) {
console.log(e)
}
}
return plugins.map((plugin) => {
2 years ago
if (plugin.name === testPluginName) plugin.url = testPluginUrl
2 years ago
return new IframePlugin(plugin)
})
}
3 years ago
async registerContextMenuItems() {
await this.call('filePanel', 'registerContextMenuItem', {
id: 'contractflattener',
name: 'flattenAContract',
label: 'Flatten',
type: [],
extension: ['.sol'],
path: [],
pattern: [],
sticky: true,
group: 5
})
await this.call('filePanel', 'registerContextMenuItem', {
id: 'nahmii-compiler',
name: 'compileCustomAction',
label: 'Compile for Nahmii',
type: [],
extension: ['.sol'],
path: [],
pattern: [],
sticky: true,
group: 6
})
2 years ago
await this.call('filePanel', 'registerContextMenuItem', {
id: 'solidityumlgen',
name: 'generateCustomAction',
label: 'Generate UML',
2 years ago
type: [],
extension: ['.sol'],
path: [],
pattern: [],
sticky: true,
group: 7
2 years ago
})
await this.call('filePanel', 'registerContextMenuItem', {
id: 'doc-gen',
name: 'generateDocsCustomAction',
label: 'Generate Docs',
type: [],
extension: ['.sol'],
path: [],
pattern: [],
sticky: true,
group: 7
})
}
6 years ago
}
5 years ago
/** @class Reference loaders.
* A loader is a get,set based object which load a workspace from a defined sources.
* (localStorage, queryParams)
**/
class PluginLoader {
3 years ago
get currentLoader() {
5 years ago
return this.loaders[this.current]
}
5 years ago
3 years ago
constructor() {
const queryParams = new QueryParams()
this.donotAutoReload = ['remixd'] // that would be a bad practice to force loading some plugins at page load.
this.loaders = {}
this.loaders.localStorage = {
set: (plugin, actives) => {
const saved = actives.filter((name) => !this.donotAutoReload.includes(name))
localStorage.setItem('workspace', JSON.stringify(saved))
},
get: () => {
return JSON.parse(localStorage.getItem('workspace'))
}
}
this.loaders.queryParams = {
set: () => {
/* Do nothing. */
},
get: () => {
const {activate} = queryParams.get()
if (!activate) return []
return activate.split(',')
}
}
this.current = queryParams.get().activate ? 'queryParams' : 'localStorage'
}
3 years ago
set(plugin, actives) {
5 years ago
this.currentLoader.set(plugin, actives)
}
3 years ago
get() {
5 years ago
return this.currentLoader.get()
}
}