add_vm_fork
yann300 2 years ago
parent e7c077b99c
commit 9860aaa256
  1. 11
      apps/remix-ide/src/app.js
  2. 102
      apps/remix-ide/src/app/tabs/abstract-provider.tsx
  3. 38
      apps/remix-ide/src/app/tabs/basic-http-provider.tsx
  4. 65
      apps/remix-ide/src/app/tabs/external-http-provider.tsx
  5. 15
      apps/remix-ide/src/app/tabs/foundry-provider.tsx
  6. 17
      apps/remix-ide/src/app/tabs/ganache-provider.tsx
  7. 17
      apps/remix-ide/src/app/tabs/hardhat-provider.tsx
  8. 48
      apps/remix-ide/src/app/tabs/vm-mainnet-fork.tsx
  9. 23
      apps/remix-ide/src/app/udapp/run-tab.js
  10. 5
      apps/remix-ide/src/blockchain/blockchain.js
  11. 21
      apps/remix-ide/src/blockchain/execution-context.js
  12. 8
      apps/remix-ide/src/blockchain/providers/vm.js
  13. 2
      apps/remix-ide/src/blockchain/providers/worker-vm.ts
  14. 2
      libs/remix-simulator/src/provider.ts
  15. 23
      libs/remix-simulator/src/vm-context.ts
  16. 7
      libs/remix-ui/run-tab/src/lib/reducers/runTab.ts

@ -27,10 +27,11 @@ import { StoragePlugin } from './app/plugins/storage'
import { Layout } from './app/panels/layout'
import { NotificationPlugin } from './app/plugins/notification'
import { Blockchain } from './blockchain/blockchain.js'
import { ExecutionContext } from './blockchain/execution-context'
import { HardhatProvider } from './app/tabs/hardhat-provider'
import { GanacheProvider } from './app/tabs/ganache-provider'
import { FoundryProvider } from './app/tabs/foundry-provider'
import { ExternalHttpProvider } from './app/tabs/external-http-provider'
import { BasicHttpProvider } from './app/tabs/basic-http-provider'
import { Injected0ptimismProvider } from './app/tabs/injected-optimism-provider'
import { InjectedArbitrumOneProvider } from './app/tabs/injected-arbitrum-one-provider'
import { FileDecorator } from './app/plugins/file-decorator'
@ -176,7 +177,8 @@ class AppComponent {
// ----------------- import content service ------------------------
const contentImport = new CompilerImports()
const blockchain = new Blockchain(Registry.getInstance().get('config').api)
const executionContext = new ExecutionContext()
const blockchain = new Blockchain(Registry.getInstance().get('config').api, executionContext)
// ----------------- compilation metadata generation service ---------
const compilerMetadataGenerator = new CompilerMetadata()
@ -196,7 +198,7 @@ class AppComponent {
const hardhatProvider = new HardhatProvider(blockchain)
const ganacheProvider = new GanacheProvider(blockchain)
const foundryProvider = new FoundryProvider(blockchain)
const externalHttpProvider = new ExternalHttpProvider(blockchain)
const externalHttpProvider = new BasicHttpProvider(blockchain)
const injected0ptimismProvider = new Injected0ptimismProvider(blockchain)
const injectedArbitrumOneProvider = new InjectedArbitrumOneProvider(blockchain)
// ----------------- convert offset to line/column service -----------
@ -240,6 +242,7 @@ class AppComponent {
this.gistHandler,
configPlugin,
blockchain,
executionContext,
contentImport,
this.themeModule,
this.localeModule,
@ -378,7 +381,7 @@ class AppComponent {
await this.appManager.activatePlugin(['sidePanel']) // activating host plugin separately
await this.appManager.activatePlugin(['home'])
await this.appManager.activatePlugin(['settings', 'config'])
await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'codeParser', 'codeFormatter', 'fileDecorator', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler'])
await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'codeParser', 'codeFormatter', 'fileDecorator', 'terminal', 'executionContext', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler'])
await this.appManager.activatePlugin(['settings'])
await this.appManager.activatePlugin(['walkthrough', 'storage', 'search', 'compileAndRun', 'recorder'])

@ -1,5 +1,4 @@
import { Plugin } from '@remixproject/engine'
import { AppModal, AlertModal, ModalTypes } from '@remix-ui/app'
import { Blockchain } from '../../blockchain/blockchain'
import { ethers } from 'ethers'
@ -21,119 +20,31 @@ export type SuccessRequest = (data: JsonDataResult) => void
export abstract class AbstractProvider extends Plugin {
provider: ethers.providers.JsonRpcProvider
blocked: boolean
blockchain: Blockchain
defaultUrl: string
connected: boolean
constructor (profile, blockchain, defaultUrl) {
constructor (profile, blockchain) {
super(profile)
this.defaultUrl = defaultUrl
this.provider = null
this.blocked = false // used to block any call when trying to recover after a failed connection.
this.connected = false
this.blockchain = blockchain
}
abstract body(): JSX.Element
abstract instanciateProvider(value: string): any
abstract init()
abstract displayName()
onDeactivation () {
this.provider = null
this.blocked = false
}
sendAsync (data: JsonDataRequest): Promise<any> {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
if (this.blocked) return reject(new Error('provider unable to connect'))
// If provider is not set, allow to open modal only when provider is trying to connect
if (!this.provider) {
let value: string
try {
value = await ((): Promise<string> => {
return new Promise((resolve, reject) => {
const modalContent: AppModal = {
id: this.profile.name,
title: this.profile.displayName,
message: this.body(),
modalType: ModalTypes.prompt,
okLabel: 'OK',
cancelLabel: 'Cancel',
validationFn: (value) => {
if (!value) return { valid: false, message: "value is empty" }
if (value.startsWith('https://') || value.startsWith('http://')) {
return {
valid: true,
message: ''
}
} else {
return {
valid: false,
message: 'the provided value should contain the protocol ( e.g starts with http:// or https:// )'
}
}
},
okFn: (value: string) => {
setTimeout(() => resolve(value), 0)
},
cancelFn: () => {
setTimeout(() => reject(new Error('Canceled')), 0)
},
hideFn: () => {
setTimeout(() => reject(new Error('Hide')), 0)
},
defaultValue: this.defaultUrl
}
this.call('notification', 'modal', modalContent)
})
})()
} catch (e) {
// the modal has been canceled/hide
const result = data.method === 'net_listening' ? 'canceled' : []
resolve({ jsonrpc: '2.0', result: result, id: data.id })
this.switchAway(false)
return
}
this.provider = new ethers.providers.JsonRpcProvider(value)
try {
setTimeout(() => {
if (!this.connected) {
this.switchAway(true)
reject('Unable to connect')
}
}, 2000)
await this.provider.detectNetwork() // this throws if the network cannot be detected
this.connected = true
} catch (e) {
this.switchAway(true)
reject('Unable to connect')
return
}
this.sendAsyncInternal(data, resolve, reject)
} else {
if (!this.provider) return reject(new Error('provider not set'))
this.sendAsyncInternal(data, resolve, reject)
}
})
}
private async switchAway (showError) {
if (!this.provider) return
this.provider = null
this.blocked = true
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)
}
await this.call('udapp', 'setEnvironmentMode', { context: 'vm', fork: 'london' })
setTimeout(_ => { this.blocked = false }, 1000) // we wait 1 second for letting remix to switch to vm
return
}
private async sendAsyncInternal (data: JsonDataRequest, resolve: SuccessRequest, reject: RejectRequest): Promise<void> {
if (this.provider) {
// Check the case where current environment is VM on UI and it still sends RPC requests
@ -144,9 +55,6 @@ export abstract class AbstractProvider extends Plugin {
const result = await this.provider.send(data.method, data.params)
resolve({ jsonrpc: '2.0', result, id: data.id })
} catch (error) {
if (error && error.message && error.message.includes('net_version') && error.message.includes('SERVER_ERROR')) {
this.switchAway(true)
}
reject(error)
}
} else {

@ -0,0 +1,38 @@
import * as packageJson from '../../../../../package.json'
import React from 'react' // eslint-disable-line
import { ExternalHttpProvider } from './external-http-provider'
import { ethers } from 'ethers'
const profile = {
name: 'basic-http-provider',
displayName: 'External Http Provider',
kind: 'provider',
description: 'External Http Provider',
methods: ['sendAsync', 'displayName'],
version: packageJson.version
}
export class BasicHttpProvider extends ExternalHttpProvider {
constructor (blockchain) {
super(profile, blockchain)
}
displayName () { return profile.displayName }
body (): JSX.Element {
return (
<div> Note: To run Anvil on your system, run:
<div className="p-1 pl-3"><b>curl -L https://foundry.paradigm.xyz | bash</b></div>
<div className="p-1 pl-3"><b>anvil</b></div>
<div className="pt-2 pb-4">
For more info, visit: <a href="https://github.com/foundry-rs/foundry" target="_blank">Foundry Documentation</a>
</div>
<div>Anvil JSON-RPC Endpoint:</div>
</div>
)
}
instanciateProvider (value): any {
return new ethers.providers.JsonRpcProvider(value)
}
}

@ -1,19 +1,62 @@
import * as packageJson from '../../../../../package.json'
import React from 'react' // eslint-disable-line
import { AppModal, AlertModal, ModalTypes } from '@remix-ui/app'
import { AbstractProvider } from './abstract-provider'
const profile = {
name: 'basic-http-provider',
displayName: 'External Http Provider',
kind: 'provider',
description: 'External Http Provider',
methods: ['sendAsync'],
version: packageJson.version
}
import { ethers } from 'ethers'
export class ExternalHttpProvider extends AbstractProvider {
constructor (blockchain) {
super(profile, blockchain, 'http://127.0.0.1:8545')
constructor (profile, blockchain) {
super(profile, blockchain)
}
displayName () { return '' }
instanciateProvider (value): any {
return new ethers.providers.JsonRpcProvider(value)
}
async init() {
let value = await ((): Promise<string> => {
return new Promise((resolve, reject) => {
const modalContent: AppModal = {
id: this.profile.name,
title: this.profile.displayName,
message: this.body(),
modalType: ModalTypes.prompt,
okLabel: 'OK',
cancelLabel: 'Cancel',
validationFn: (value) => {
if (!value) return { valid: false, message: "value is empty" }
if (value.startsWith('https://') || value.startsWith('http://')) {
return {
valid: true,
message: ''
}
} else {
return {
valid: false,
message: 'the provided value should contain the protocol ( e.g starts with http:// or https:// )'
}
}
},
okFn: (value: string) => {
setTimeout(() => resolve(value), 0)
},
cancelFn: () => {
setTimeout(() => reject(new Error('Canceled')), 0)
},
hideFn: () => {
setTimeout(() => reject(new Error('Hide')), 0)
},
defaultValue: 'http://127.0.0.1:8545'
}
this.call('notification', 'modal', modalContent)
})
})()
if (value) {
this.provider = this.instanciateProvider(value)
} else
throw new Error('value cannot be empty')
}
body (): JSX.Element {

@ -1,21 +1,24 @@
import * as packageJson from '../../../../../package.json'
import React from 'react' // eslint-disable-line
import { AbstractProvider } from './abstract-provider'
import { ExternalHttpProvider } from './external-http-provider'
import { ethers } from 'ethers'
const profile = {
name: 'foundry-provider',
displayName: 'Foundry Provider',
kind: 'provider',
description: 'Foundry Anvil provider',
methods: ['sendAsync'],
methods: ['sendAsync', 'displayName'],
version: packageJson.version
}
export class FoundryProvider extends AbstractProvider {
export class FoundryProvider extends ExternalHttpProvider {
constructor (blockchain) {
super(profile, blockchain, 'http://127.0.0.1:8545')
super(profile, blockchain)
}
displayName () { return profile.displayName }
body (): JSX.Element {
return (
<div> Note: To run Anvil on your system, run:
@ -28,4 +31,8 @@ export class FoundryProvider extends AbstractProvider {
</div>
)
}
instanciateProvider (value): any {
return new ethers.providers.JsonRpcProvider(value)
}
}

@ -1,25 +1,24 @@
import * as packageJson from '../../../../../package.json'
import { Plugin } from '@remixproject/engine'
import { AppModal, AlertModal, ModalTypes } from '@remix-ui/app'
import React from 'react' // eslint-disable-line
import { Blockchain } from '../../blockchain/blockchain'
import { ethers } from 'ethers'
import { AbstractProvider } from './abstract-provider'
import { ExternalHttpProvider } from './external-http-provider'
const profile = {
name: 'ganache-provider',
displayName: 'Ganache Provider',
kind: 'provider',
description: 'Truffle Ganache provider',
methods: ['sendAsync'],
methods: ['sendAsync', 'displayName'],
version: packageJson.version
}
export class GanacheProvider extends AbstractProvider {
export class GanacheProvider extends ExternalHttpProvider {
constructor (blockchain) {
super(profile, blockchain, 'http://127.0.0.1:8545')
super(profile, blockchain)
}
displayName () { return profile.displayName }
body (): JSX.Element {
return (
<div> Note: To run Ganache on your system, run:
@ -32,4 +31,8 @@ export class GanacheProvider extends AbstractProvider {
</div>
)
}
instanciateProvider (value): any {
return new ethers.providers.JsonRpcProvider(value)
}
}

@ -1,25 +1,24 @@
import * as packageJson from '../../../../../package.json'
import { Plugin } from '@remixproject/engine'
import { AppModal, AlertModal, ModalTypes } from '@remix-ui/app'
import React from 'react' // eslint-disable-line
import { Blockchain } from '../../blockchain/blockchain'
import { ethers } from 'ethers'
import { AbstractProvider } from './abstract-provider'
import { ExternalHttpProvider } from './external-http-provider'
const profile = {
name: 'hardhat-provider',
displayName: 'Hardhat Provider',
kind: 'provider',
description: 'Hardhat provider',
methods: ['sendAsync'],
methods: ['sendAsync', 'displayName'],
version: packageJson.version
}
export class HardhatProvider extends AbstractProvider {
export class HardhatProvider extends ExternalHttpProvider {
constructor (blockchain) {
super(profile, blockchain, 'http://127.0.0.1:8545')
super(profile, blockchain)
}
displayName () { return profile.displayName }
body (): JSX.Element {
return (
<div> Note: To run Hardhat network node on your system, go to hardhat project folder and run command:
@ -31,4 +30,8 @@ export class HardhatProvider extends AbstractProvider {
</div>
)
}
instanciateProvider (value): any {
return new ethers.providers.JsonRpcProvider(value)
}
}

@ -0,0 +1,48 @@
import * as packageJson from '../../../../../package.json'
import React from 'react' // eslint-disable-line
import { AppModal, AlertModal, ModalTypes } from '@remix-ui/app'
import { ethers } from 'ethers'
import { AbstractProvider } from './abstract-provider'
const profile = {
name: 'vm-mainnet-fork',
displayName: 'Mainnet fork - Remix VM',
kind: 'provider',
description: 'Mainnet fork - Remix VM',
methods: ['sendAsync'],
version: packageJson.version
}
export class VMMainnetFork extends AbstractProvider {
urlInput: JSX.Element
blockInput: JSX.Element
forkInput: JSX.Element
constructor (blockchain) {
super(profile, blockchain)
}
displayName () { return profile.displayName }
nodeUrl () {
return 'https://rpc.archivenode.io/e50zmkroshle2e2e50zm0044i7ao04ym'
}
blockNumber () {
return 'latest'
}
async init() {
this.provider = this.blockchain.providers.vm.provider
}
body (): JSX.Element {
return (
<>
</>
)
}
instanciateProvider (value): any {
return this.provider
}
}

@ -102,7 +102,6 @@ export class RunTab extends ViewPlugin {
const udapp = this // eslint-disable-line
await this.call('blockchain', 'addProvider', {
name: 'Hardhat Provider',
isInjected: false,
provider: {
async sendAsync (payload, callback) {
@ -117,7 +116,6 @@ export class RunTab extends ViewPlugin {
})
await this.call('blockchain', 'addProvider', {
name: 'Ganache Provider',
isInjected: false,
provider: {
async sendAsync (payload, callback) {
@ -132,7 +130,6 @@ export class RunTab extends ViewPlugin {
})
await this.call('blockchain', 'addProvider', {
name: 'Foundry Provider',
isInjected: false,
provider: {
async sendAsync (payload, callback) {
@ -147,7 +144,6 @@ export class RunTab extends ViewPlugin {
})
await this.call('blockchain', 'addProvider', {
name: 'Wallet Connect',
isInjected: false,
provider: {
async sendAsync (payload, callback) {
@ -162,7 +158,6 @@ export class RunTab extends ViewPlugin {
})
await this.call('blockchain', 'addProvider', {
name: 'External Http Provider',
provider: {
async sendAsync (payload, callback) {
try {
@ -176,7 +171,6 @@ export class RunTab extends ViewPlugin {
})
await this.call('blockchain', 'addProvider', {
name: 'Optimism Provider',
isInjected: true,
provider: {
async sendAsync (payload, callback) {
@ -191,7 +185,6 @@ export class RunTab extends ViewPlugin {
})
await this.call('blockchain', 'addProvider', {
name: 'Arbitrum One Provider',
isInjected: true,
provider: {
async sendAsync (payload, callback) {
@ -204,6 +197,22 @@ export class RunTab extends ViewPlugin {
}
}
})
/*
await this.call('blockchain', 'addProvider', {
name: 'Mainnet fork - Remix VM (London)',
isInjected: false,
provider: {
async sendAsync (payload, callback) {
try {
const result = await udapp.call('injected-arbitrum-one-provider', 'sendAsync', payload)
callback(null, result)
} catch (e) {
callback(e)
}
}
}
})
*/
}
writeFile (fileName, content) {

@ -5,7 +5,6 @@ import { Plugin } from '@remixproject/engine'
import { toBuffer, addHexPrefix } from 'ethereumjs-util'
import { EventEmitter } from 'events'
import { format } from 'util'
import { ExecutionContext } from './execution-context'
import VMProvider from './providers/vm.js'
import InjectedProvider from './providers/injected.js'
import NodeProvider from './providers/node.js'
@ -28,10 +27,10 @@ const profile = {
export class Blockchain extends Plugin {
// NOTE: the config object will need to be refactored out in remix-lib
constructor (config) {
constructor (config, executionContext) {
super(profile)
this.event = new EventManager()
this.executionContext = new ExecutionContext()
this.executionContext = executionContext
this.events = new EventEmitter()
this.config = config

@ -1,8 +1,10 @@
/* global ethereum */
'use strict'
import Web3 from 'web3'
import { Plugin } from '@remixproject/engine'
import { execution } from '@remix-project/remix-lib'
import EventManager from '../lib/events'
const packageJson = require('../../../../package.json')
const _paq = window._paq = window._paq || []
let web3
@ -16,11 +18,20 @@ if (typeof window !== 'undefined' && typeof window.ethereum !== 'undefined') {
const noInjectedProviderMsg = 'No injected provider found. Make sure your provider (e.g. MetaMask) is active and running (when recently activated you may have to reload the page).'
const profile = {
name: 'executionContext',
displayName: 'executionContext',
description: 'executionContext - Logic',
methods: [],
version: packageJson.version
}
/*
trigger contextChanged, web3EndpointChanged
*/
export class ExecutionContext {
export class ExecutionContext extends Plugin {
constructor () {
super(profile)
this.event = new EventManager()
this.executionContext = 'vm'
this.lastBlock = null
@ -120,6 +131,10 @@ export class ExecutionContext {
}
}
getCustomNetWorks () {
return this.customNetWorks[this.executionContext]
}
removeProvider (name) {
if (name && this.customNetWorks[name]) {
if (this.executionContext === name) this.setContext('vm', null, null, null)
@ -182,8 +197,10 @@ export class ExecutionContext {
if (this.customNetWorks[context]) {
var network = this.customNetWorks[context]
await this.call(network.name, 'init')
const displayName = await this.call(network.name, 'displayName')
if (!this.customNetWorks[context].isInjected) {
this.setProviderFromEndpoint(network.provider, { context: network.name }, (error) => {
this.setProviderFromEndpoint(network.provider, { context: displayName }, (error) => {
if (error) infoCb(error)
cb()
})

@ -5,6 +5,7 @@ class VMProvider {
constructor (executionContext) {
this.executionContext = executionContext
this.worker = null
this.provider = null
}
getAccounts (cb) {
@ -20,7 +21,8 @@ class VMProvider {
if (this.worker) this.worker.terminate()
this.accounts = {}
this.worker = new Worker(new URL('./worker-vm', import.meta.url))
this.worker.postMessage({ cmd: 'init', fork: this.executionContext.getCurrentFork(), rawContext: this.executionContext.getCurrentRawContext() })
const customNetWork = this.executionContext.getCustomNetWorks()
this.worker.postMessage({ cmd: 'init', fork: this.executionContext.getCurrentFork(), nodeUrl: customNetWork?.nodeUrl(), blockNumber: customNetWork?.blockNumber() })
let incr = 0
const stamps = {}
@ -29,7 +31,7 @@ class VMProvider {
stamps[msg.data.stamp](msg.data.error, msg.data.result)
}
})
const provider = {
this.provider = {
sendAsync: (query, callback) => {
const stamp = Date.now() + incr
incr++
@ -37,7 +39,7 @@ class VMProvider {
this.worker.postMessage({ cmd: 'sendAsync', query, stamp })
}
}
this.web3 = new Web3(provider)
this.web3 = new Web3(this.provider)
extend(this.web3)
this.accounts = {}
this.executionContext.setWeb3('vm', this.web3)

@ -6,7 +6,7 @@ self.onmessage = (e: MessageEvent) => {
switch (data.cmd) {
case 'init':
{
provider = new Provider({ fork: data.fork, rawContext: data.rawContext })
provider = new Provider({ fork: data.fork, nodeUrl: data.nodeUrl, blockNumber: data.blockNumber})
if (provider) provider.init()
break
}

@ -23,7 +23,7 @@ export class Provider {
constructor (options: Record<string, unknown> = {}) {
this.options = options
this.connected = true
this.vmContext = new VMContext(options['fork'] as string, options['rawContext'] as string)
this.vmContext = new VMContext(options['fork'] as string, options['nodeUrl'] as string, options['blockNumber'] as any)
this.Accounts = new Web3Accounts(this.vmContext)
this.Transactions = new Transactions(this.vmContext)

@ -104,13 +104,15 @@ export class VMContext {
web3vm: VmProxy
logsManager: any // LogsManager
exeResults: Record<string, Transaction>
rawContext: string
nodeUrl: string
blockNumber: number | 'latest'
constructor (fork?: string, rawContext?: string) {
constructor (fork?: string, nodeUrl?: string, blockNumber?: number | 'latest') {
this.blockGasLimitDefault = 4300000
this.blockGasLimit = this.blockGasLimitDefault
this.currentFork = fork || 'london'
this.rawContext = rawContext
this.nodeUrl = nodeUrl
this.blockNumber = blockNumber || 'latest'
this.blocks = {}
this.latestBlockNumber = "0x0"
@ -127,14 +129,15 @@ export class VMContext {
async createVm (hardfork) {
let stateManager: StateManager
if (this.rawContext && this.rawContext === 'vm-fork-main') {
console.log('set state manager')
const url = 'https://rpc.archivenode.io/e50zmkroshle2e2e50zm0044i7ao04ym'
const provider = new ethers.providers.StaticJsonRpcProvider(url)
const blockNumber = await provider.getBlockNumber()
if (this.nodeUrl && this.nodeUrl === 'vm-fork-main') {
let block = this.blockNumber
if (this.blockNumber === 'latest') {
const provider = new ethers.providers.StaticJsonRpcProvider(this.nodeUrl)
block = await provider.getBlockNumber()
}
stateManager = new EthersStateManager({
provider: url,
blockTag: BigInt(blockNumber)
provider: this.nodeUrl,
blockTag: BigInt(block)
})
} else
stateManager = new StateManagerCommonStorageDump()

@ -133,13 +133,6 @@ export const runTabInitialState: RunTabState = {
value: 'vm-berlin',
fork: 'berlin',
content: 'Remix VM (Berlin)'
}, {
id: 'vm-mode-fork-main',
dataId: 'settingsVMForkMode',
title: 'Execution environment is local to Remix. Data is only saved to browser memory and will vanish upon reload.',
value: 'vm-fork-main',
fork: 'london',
content: 'Mainnet fork - Remix VM (London)'
}, {
id: 'injected-mode',
dataId: 'settingsInjectedMode',

Loading…
Cancel
Save