diff --git a/apps/remix-ide/src/blockchain/blockchain.tsx b/apps/remix-ide/src/blockchain/blockchain.tsx index ac972952bd..18e11ec102 100644 --- a/apps/remix-ide/src/blockchain/blockchain.tsx +++ b/apps/remix-ide/src/blockchain/blockchain.tsx @@ -1,7 +1,7 @@ import React from 'react' // eslint-disable-line import {fromWei, toBigInt, toWei} from 'web3-utils' import {Plugin} from '@remixproject/engine' -import {toBuffer, addHexPrefix, bufferToHex} from '@ethereumjs/util' +import {toBuffer, addHexPrefix} from '@ethereumjs/util' import {EventEmitter} from 'events' import {format} from 'util' import {ExecutionContext} from './execution-context' @@ -135,7 +135,9 @@ export class Blockchain extends Plugin { setupEvents() { this.executionContext.event.register('contextChanged', async (context) => { - await this.resetEnvironment() + console.log('context changed', context) + // reset environment to last known state of the context + await this.loadContext(context) this._triggerEvent('contextChanged', [context]) this.detectNetwork((error, network) => { this.networkStatus = {network, error} @@ -164,7 +166,7 @@ export class Blockchain extends Plugin { } setupProviders() { - const vmProvider = new VMProvider(this.executionContext, this) + const vmProvider = new VMProvider(this.executionContext) this.providers = {} this.providers['vm'] = vmProvider this.providers.injected = new InjectedProvider(this.executionContext) @@ -643,8 +645,16 @@ export class Blockchain extends Plugin { }) } - async resetEnvironment() { - await this.getCurrentProvider().resetEnvironment() + async loadContext(context: string) { + const contextExists = await this.call('fileManager', 'exists', '.context') + if (contextExists) { + const stateDb = await this.call('fileManager', 'readFile', `.states/${context}/state.json`) + + await this.getCurrentProvider().loadContext(stateDb) + } else { + await this.getCurrentProvider().resetEnvironment() + } + // TODO: most params here can be refactored away in txRunner const web3Runner = new TxRunnerWeb3( { @@ -890,27 +900,9 @@ export class Blockchain extends Plugin { let returnValue = null if (isVM) { if (!tx.useCall) { - // TODO: this won't save the state for transactions executed outside of the UI (dor instande from a script execution). - setTimeout(async() => { - const root = await this.web3().remix.getStateTrieRoot() - const db = await this.web3().remix.getStateDb() - const state = { - root, - db: Object.fromEntries(db._database) - } - console.log('saving', state) - const stringifyed = JSON.stringify(state, (key, value) => { - if (key === 'root') { - return bufferToHex(value) - } else if (key === 'db') { - return value - } else if (key === '') { - return value - } - return bufferToHex(value) - }, '\t') - this.call('fileManager', 'writeFile', '.states/state.json', stringifyed) - }, 500) + await this.executionContext.getStateDetails().then((state) => { + this.call('fileManager', 'writeFile', `.states/${this.executionContext.getProvider()}/state.json`, state) + }) } const hhlogs = await this.web3().remix.getHHLogsForTx(txResult.transactionHash) diff --git a/apps/remix-ide/src/blockchain/execution-context.js b/apps/remix-ide/src/blockchain/execution-context.js index 7c33bb4f27..e6a5eace08 100644 --- a/apps/remix-ide/src/blockchain/execution-context.js +++ b/apps/remix-ide/src/blockchain/execution-context.js @@ -3,6 +3,7 @@ import Web3 from 'web3' import { execution } from '@remix-project/remix-lib' import EventManager from '../lib/events' +import {bufferToHex} from '@ethereumjs/util' const _paq = window._paq = window._paq || [] let web3 @@ -71,35 +72,44 @@ export class ExecutionContext { } detectNetwork (callback) { - if (this.isVM()) { - callback(null, { id: '-', name: 'VM' }) - } else { - if (!web3.currentProvider) { - return callback('No provider set') - } - const cb = (err, id) => { - let name = null - if (err) name = 'Unknown' - // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md - else if (id === 1) name = 'Main' - else if (id === 3) name = 'Ropsten' - else if (id === 4) name = 'Rinkeby' - else if (id === 5) name = 'Goerli' - else if (id === 42) name = 'Kovan' - else if (id === 11155111) name = 'Sepolia' - else name = 'Custom' - - if (id === 1) { - web3.eth.getBlock(0).then((block) => { - if (block && block.hash !== this.mainNetGenesisHash) name = 'Custom' - callback(err, { id, name, lastBlock: this.lastBlock, currentFork: this.currentFork }) - }).catch((error) => callback(error)) - } else { - callback(err, { id, name, lastBlock: this.lastBlock, currentFork: this.currentFork }) + return new Promise((resolve, reject) => { + if (this.isVM()) { + callback && callback(null, { id: '-', name: 'VM' }) + return resolve({ id: '-', name: 'VM' }) + } else { + if (!web3.currentProvider) { + callback && callback('No provider set') + return reject('No provider set') + } + const cb = (err, id) => { + let name = null + if (err) name = 'Unknown' + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md + else if (id === 1) name = 'Main' + else if (id === 3) name = 'Ropsten' + else if (id === 4) name = 'Rinkeby' + else if (id === 5) name = 'Goerli' + else if (id === 42) name = 'Kovan' + else if (id === 11155111) name = 'Sepolia' + else name = 'Custom' + + if (id === 1) { + web3.eth.getBlock(0).then((block) => { + if (block && block.hash !== this.mainNetGenesisHash) name = 'Custom' + callback && callback(err, { id, name, lastBlock: this.lastBlock, currentFork: this.currentFork }) + return resolve({ id, name, lastBlock: this.lastBlock, currentFork: this.currentFork }) + }).catch((error) => { + callback && callback(error) + return reject(error) + }) + } else { + callback && callback(err, { id, name, lastBlock: this.lastBlock, currentFork: this.currentFork }) + return resolve({ id, name, lastBlock: this.lastBlock, currentFork: this.currentFork }) + } } + web3.eth.net.getId().then(id=>cb(null,parseInt(id))).catch(err=>cb(err)) } - web3.eth.net.getId().then(id=>cb(null,parseInt(id))).catch(err=>cb(err)) - } + }) } removeProvider (name) { @@ -195,4 +205,26 @@ export class ExecutionContext { return transactionDetailsLinks[network] + hash } } + + async getStateDetails() { + // TODO: this won't save the state for transactions executed outside of the UI (for instance from a script execution). + const root = await this.web3().remix.getStateTrieRoot() + const db = await this.web3().remix.getStateDb() + const state = { + root, + db: Object.fromEntries(db._database) + } + const stringifyed = JSON.stringify(state, (key, value) => { + if (key === 'root') { + return bufferToHex(value) + } else if (key === 'db') { + return value + } else if (key === '') { + return value + } + return bufferToHex(value) + }, '\t') + + return stringifyed + } } diff --git a/apps/remix-ide/src/blockchain/providers/injected.ts b/apps/remix-ide/src/blockchain/providers/injected.ts index 32b4938f70..6c021b534f 100644 --- a/apps/remix-ide/src/blockchain/providers/injected.ts +++ b/apps/remix-ide/src/blockchain/providers/injected.ts @@ -27,6 +27,10 @@ export class InjectedProvider { /* Do nothing. */ } + async loadContext (context) { + /* Do nothing. */ + } + async getBalanceInEther (address) { const balance = await this.executionContext.web3().eth.getBalance(address) const balInString = balance.toString(10) diff --git a/apps/remix-ide/src/blockchain/providers/node.ts b/apps/remix-ide/src/blockchain/providers/node.ts index c77421e045..301c006936 100644 --- a/apps/remix-ide/src/blockchain/providers/node.ts +++ b/apps/remix-ide/src/blockchain/providers/node.ts @@ -33,6 +33,10 @@ export class NodeProvider { /* Do nothing. */ } + async loadContext (context) { + /* Do nothing. */ + } + async getBalanceInEther (address) { const balance = await this.executionContext.web3().eth.getBalance(address) const balInString = balance.toString(10) diff --git a/apps/remix-ide/src/blockchain/providers/vm.ts b/apps/remix-ide/src/blockchain/providers/vm.ts index 43266c5250..ced901e288 100644 --- a/apps/remix-ide/src/blockchain/providers/vm.ts +++ b/apps/remix-ide/src/blockchain/providers/vm.ts @@ -14,9 +14,7 @@ export class VMProvider { sendAsync: (query: JSONRPCRequestPayload, callback: JSONRPCResponseCallback) => void } newAccountCallback: {[stamp: number]: (error: Error, address: string) => void} - plugin: Plugin - constructor (executionContext: ExecutionContext, plugin: Plugin) { - this.plugin = plugin + constructor (executionContext: ExecutionContext) { this.executionContext = executionContext this.worker = null this.provider = null @@ -32,66 +30,41 @@ export class VMProvider { } async resetEnvironment () { + if (this.worker) { + const provider = this.executionContext.getProviderObject() + + this.worker.postMessage({ + cmd: 'init', + fork: this.executionContext.getCurrentFork(), + nodeUrl: provider?.options['nodeUrl'], + blockNumber: provider?.options['blockNumber'] + }) + } else { + this.worker = new Worker(new URL('./worker-vm', import.meta.url)) + this.setWorkerEventListeners(this.worker) + const provider = this.executionContext.getProviderObject() + + this.worker.postMessage({ + cmd: 'init', + fork: this.executionContext.getCurrentFork(), + nodeUrl: provider?.options['nodeUrl'], + blockNumber: provider?.options['blockNumber'] + }) + } + } + + async loadContext (stringifiedStateDb: string) { if (this.worker) this.worker.terminate() this.worker = new Worker(new URL('./worker-vm', import.meta.url)) + this.setWorkerEventListeners(this.worker) const provider = this.executionContext.getProviderObject() - let incr = 0 - const stamps = {} - - return new Promise((resolve, reject) => { - this.worker.addEventListener('message', (msg) => { - if (msg.data.cmd === 'sendAsyncResult' && stamps[msg.data.stamp]) { - if (stamps[msg.data.stamp].callback) { - stamps[msg.data.stamp].callback(msg.data.error, msg.data.result) - return - } - if (msg.data.error) { - stamps[msg.data.stamp].reject(msg.data.error) - } else { - stamps[msg.data.stamp].resolve(msg.data.result) - } - } else if (msg.data.cmd === 'initiateResult') { - if (!msg.data.error) { - this.provider = { - sendAsync: (query, callback) => { - return new Promise((resolve, reject) => { - const stamp = Date.now() + incr - incr++ - stamps[stamp] = { callback, resolve, reject } - this.worker.postMessage({ cmd: 'sendAsync', query, stamp }) - }) - } - } - this.web3 = new Web3(this.provider as LegacySendAsyncProvider) - this.web3.setConfig({ defaultTransactionType: '0x0' }) - extend(this.web3) - this.executionContext.setWeb3(this.executionContext.getProvider(), this.web3) - resolve({}) - } else { - reject(new Error(msg.data.error)) - } - } else if (msg.data.cmd === 'newAccountResult') { - if (this.newAccountCallback[msg.data.stamp]) { - this.newAccountCallback[msg.data.stamp](msg.data.error, msg.data.result) - delete this.newAccountCallback[msg.data.stamp] - } - } - }) - const init = async () => { - let stateDb - if (await this.plugin.call('fileManager', 'exists', '.states/state.json')) { - stateDb = await this.plugin.call('fileManager', 'readFile', '.states/state.json') - } - this.worker.postMessage({ - cmd: 'init', - fork: this.executionContext.getCurrentFork(), - nodeUrl: provider?.options['nodeUrl'], - blockNumber: provider?.options['blockNumber'], - stateDb - }) - } - init() + this.worker.postMessage({ + cmd: 'init', + fork: this.executionContext.getCurrentFork(), + nodeUrl: provider?.options['nodeUrl'], + blockNumber: provider?.options['blockNumber'], + stateDb: stringifiedStateDb }) } @@ -131,4 +104,49 @@ export class VMProvider { getProvider () { return this.executionContext.getProvider() } + + private setWorkerEventListeners (worker: Worker) { + if (!worker) throw new Error('Worker not initialized') + let incr = 0 + const stamps = {} + + worker.addEventListener('message', (msg) => { + if (msg.data.cmd === 'sendAsyncResult' && stamps[msg.data.stamp]) { + if (stamps[msg.data.stamp].callback) { + stamps[msg.data.stamp].callback(msg.data.error, msg.data.result) + return + } + if (msg.data.error) { + stamps[msg.data.stamp].reject(msg.data.error) + } else { + stamps[msg.data.stamp].resolve(msg.data.result) + } + } else if (msg.data.cmd === 'initiateResult') { + if (!msg.data.error) { + this.provider = { + sendAsync: (query, callback) => { + return new Promise((resolve, reject) => { + const stamp = Date.now() + incr + incr++ + stamps[stamp] = { callback, resolve, reject } + worker.postMessage({ cmd: 'sendAsync', query, stamp }) + }) + } + } + this.web3 = new Web3(this.provider as LegacySendAsyncProvider) + this.web3.setConfig({ defaultTransactionType: '0x0' }) + extend(this.web3) + this.executionContext.setWeb3(this.executionContext.getProvider(), this.web3) + } else { + console.error(msg.data.error) + throw new Error(msg.data.error) + } + } else if (msg.data.cmd === 'newAccountResult') { + if (this.newAccountCallback[msg.data.stamp]) { + this.newAccountCallback[msg.data.stamp](msg.data.error, msg.data.result) + delete this.newAccountCallback[msg.data.stamp] + } + } + }) + } } diff --git a/libs/remix-simulator/src/provider.ts b/libs/remix-simulator/src/provider.ts index 8ad22b35ec..98c358cba7 100644 --- a/libs/remix-simulator/src/provider.ts +++ b/libs/remix-simulator/src/provider.ts @@ -32,8 +32,16 @@ export type State = { root: Buffer } +export type ProviderOptions = { + fork: string, + nodeUrl: string, + blockNumber: number | 'latest', + stateDb: State, + logDetails?: boolean +} + export class Provider { - options: Record + options: ProviderOptions vmContext Accounts Transactions @@ -42,10 +50,10 @@ export class Provider { initialized: boolean pendingRequests: Array - constructor (options: Record = {}) { + constructor (options: ProviderOptions = {} as ProviderOptions) { this.options = options this.connected = true - this.vmContext = new VMContext(options['fork'] as string, options['nodeUrl'] as string, options['blockNumber'] as (number | 'latest'), options['stateDb'] as State) + this.vmContext = new VMContext(options['fork'], options['nodeUrl'], options['blockNumber'], options['stateDb']) this.Accounts = new Web3Accounts(this.vmContext) this.Transactions = new Transactions(this.vmContext) diff --git a/libs/remix-simulator/src/vm-context.ts b/libs/remix-simulator/src/vm-context.ts index 405ad3eed2..b449686c16 100644 --- a/libs/remix-simulator/src/vm-context.ts +++ b/libs/remix-simulator/src/vm-context.ts @@ -331,9 +331,9 @@ export class VMContext { exeResults: Record nodeUrl: string blockNumber: number | 'latest' - stateDb: any + stateDb: State - constructor (fork?: string, nodeUrl?: string, blockNumber?: number | 'latest', stateDb?: any) { + constructor (fork?: string, nodeUrl?: string, blockNumber?: number | 'latest', stateDb?: State) { this.blockGasLimitDefault = 4300000 this.blockGasLimit = this.blockGasLimitDefault this.currentFork = fork || 'merge' diff --git a/libs/remix-ui/run-tab/src/lib/types/blockchain.d.ts b/libs/remix-ui/run-tab/src/lib/types/blockchain.d.ts index a0b6bd0636..6cf9ffc964 100644 --- a/libs/remix-ui/run-tab/src/lib/types/blockchain.d.ts +++ b/libs/remix-ui/run-tab/src/lib/types/blockchain.d.ts @@ -61,6 +61,7 @@ export class Blockchain extends Plugin { /** Listen on New Transaction. (Cannot be done inside constructor because txlistener doesn't exist yet) */ startListening(txlistener: any): void; resetEnvironment(): Promise; + loadContext(): Promise; /** * Create a VM Account * @param {{privateKey: string, balance: string}} newAccount The new account to create diff --git a/libs/remix-ui/run-tab/src/lib/types/injected.d.ts b/libs/remix-ui/run-tab/src/lib/types/injected.d.ts index 2c2c4fb3fe..562f6709b7 100644 --- a/libs/remix-ui/run-tab/src/lib/types/injected.d.ts +++ b/libs/remix-ui/run-tab/src/lib/types/injected.d.ts @@ -5,6 +5,7 @@ declare class InjectedProvider { getAccounts(cb: any): any; newAccount(passwordPromptCb: any, cb: any): void; resetEnvironment(): Promise; + loadContext(context: any): Promise; getBalanceInEther(address: any): Promise; getGasPrice(cb: any): void; signMessage(message: any, account: any, _passphrase: any, cb: any): void; diff --git a/libs/remix-ui/run-tab/src/lib/types/node.d.ts b/libs/remix-ui/run-tab/src/lib/types/node.d.ts index 84169e3be7..5e7e8f49d7 100644 --- a/libs/remix-ui/run-tab/src/lib/types/node.d.ts +++ b/libs/remix-ui/run-tab/src/lib/types/node.d.ts @@ -6,6 +6,7 @@ declare class NodeProvider { getAccounts(cb: any): any; newAccount(passwordPromptCb: any, cb: any): any; resetEnvironment(): Promise; + loadContext(context: any): Promise; getBalanceInEther(address: any): Promise; getGasPrice(cb: any): void; signMessage(message: any, account: any, passphrase: any, cb: any): void; diff --git a/libs/remix-ui/run-tab/src/lib/types/vm.d.ts b/libs/remix-ui/run-tab/src/lib/types/vm.d.ts index ff602d4f26..52fc498c5f 100644 --- a/libs/remix-ui/run-tab/src/lib/types/vm.d.ts +++ b/libs/remix-ui/run-tab/src/lib/types/vm.d.ts @@ -4,6 +4,7 @@ declare class VMProvider { executionContext: any; getAccounts(cb: any): void; resetEnvironment(): Promise; + loadEnvironment(stringifiedStateDb: string): Promise; accounts: any; RemixSimulatorProvider: any; web3: any;