diff --git a/apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts b/apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts
index cebc84bc93..943292cc82 100644
--- a/apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts
+++ b/apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts
@@ -82,21 +82,21 @@ module.exports = {
instanceAddress = address
console.log('instanceAddress', instanceAddress)
browser
- .waitForElementVisible(`#instance${instanceAddress} [data-id="instanceContractBal"]`)
+ .waitForElementVisible(`#instance${instanceAddress} [data-id="instanceContractBal"]`)
//*[@id="instance0xbBF289D846208c16EDc8474705C748aff07732dB" and contains(.,"Balance") and contains(.,'0.000000000000000111')]
- .waitForElementVisible({
- locateStrategy: 'xpath',
- selector: `//*[@id="instance${instanceAddress}" and contains(.,"Balance") and contains(.,'0.000000000000000111')]`,
- timeout: 60000
- })
+ .waitForElementVisible({
+ locateStrategy: 'xpath',
+ selector: `//*[@id="instance${instanceAddress}" and contains(.,"Balance") and contains(.,'0.000000000000000111')]`,
+ timeout: 60000
+ })
//.waitForElementContainsText(`#instance${instanceAddress} [data-id="instanceContractBal"]`, 'Balance: 0.000000000000000111 ETH', 60000)
- .clickFunction('sendSomeEther - transact (not payable)', { types: 'uint256 num', values: '2' })
- .pause(1000)
- .waitForElementVisible({
- locateStrategy: 'xpath',
- selector: `//*[@id="instance${instanceAddress}" and contains(.,"Balance") and contains(.,'0.000000000000000109')]`,
- timeout: 60000
- })
+ .clickFunction('sendSomeEther - transact (not payable)', { types: 'uint256 num', values: '2' })
+ .pause(1000)
+ .waitForElementVisible({
+ locateStrategy: 'xpath',
+ selector: `//*[@id="instance${instanceAddress}" and contains(.,"Balance") and contains(.,'0.000000000000000109')]`,
+ timeout: 60000
+ })
})
},
@@ -238,6 +238,95 @@ module.exports = {
.executeScriptInTerminal('web3.eth.getAccounts()')
.journalLastChildIncludes('[ "0x76a3ABb5a12dcd603B52Ed22195dED17ee82708f" ]')
.end()
+ },
+
+ 'Should ensure that save environment state is checked by default #group4 #group5': function (browser: NightwatchBrowser) {
+ browser.waitForElementPresent('*[data-id="remixIdeSidePanel"]')
+ .clickLaunchIcon('settings')
+ .waitForElementPresent('[data-id="settingsEnableSaveEnvStateLabel"]')
+ .scrollInto('[data-id="settingsEnableSaveEnvStateLabel"]')
+ .verify.elementPresent('[data-id="settingsEnableSaveEnvState"]:checked')
+ },
+
+ 'Should deploy default storage contract; store value and ensure that state is saved. #group4 #group5': function (browser: NightwatchBrowser) {
+ browser
+ .clickLaunchIcon('filePanel')
+ .click('*[data-id="treeViewLitreeViewItemcontracts"]')
+ .openFile('contracts/1_Storage.sol')
+ .pause(5000)
+ .clickLaunchIcon('udapp')
+ .waitForElementPresent('*[data-id="Deploy - transact (not payable)"]')
+ .click('*[data-id="Deploy - transact (not payable)"]')
+ .waitForElementPresent('#instance0xd9145CCE52D386f254917e481eB44e9943F39138')
+ .clickInstance(0)
+ .clickFunction('store - transact (not payable)', { types: 'uint256 num', values: '10' })
+ .clickFunction('retrieve - call')
+ .waitForElementContainsText('[data-id="treeViewLi0"]', 'uint256: 10')
+ .clickLaunchIcon('filePanel')
+ .openFile('.states/vm-shanghai/state.json')
+ .getEditorValue((content) => {
+ browser
+ .assert.ok(content.includes('"latestBlockNumber": "0x02"'), 'State is saved')
+ })
+ },
+
+ 'Should load state after page refresh #group4': function (browser: NightwatchBrowser) {
+ browser.refreshPage()
+ .waitForElementVisible('*[data-id="remixIdeSidePanel"]')
+ .click('*[data-id="treeViewLitreeViewItemcontracts"]')
+ .openFile('contracts/1_Storage.sol')
+ .addAtAddressInstance('0xd9145CCE52D386f254917e481eB44e9943F39138', true, true, false)
+ .clickInstance(0)
+ .clickFunction('retrieve - call')
+ .waitForElementContainsText('[data-id="treeViewLi0"]', 'uint256: 10')
+ },
+
+ 'Should save state after running web3 script #group4': function (browser: NightwatchBrowser) {
+ browser
+ .clickLaunchIcon('settings')
+ .waitForElementPresent('[data-id="settingsTabGenerateContractMetadataLabel"]')
+ .click('[data-id="settingsTabGenerateContractMetadataLabel"]')
+ .verify.elementPresent('[data-id="settingsTabGenerateContractMetadata"]:checked')
+ .clickLaunchIcon('solidity')
+ .click('.remixui_compilerConfigSection')
+ .setValue('#evmVersionSelector', 'london')
+ .click('*[data-id="compilerContainerCompileBtn"]')
+ .pause(5000)
+ .clickLaunchIcon('udapp')
+ .switchEnvironment('vm-london')
+ .clickLaunchIcon('filePanel')
+ .click('*[data-id="treeViewLitreeViewItemscripts"]')
+ .openFile('scripts/deploy_with_web3.ts')
+ .click('[data-id="play-editor"]')
+ .waitForElementPresent('[data-id="treeViewDivDraggableItem.states/vm-london/state.json"]')
+ .click('[data-id="treeViewDivDraggableItem.states/vm-london/state.json"]')
+ .pause(100000)
+ .getEditorValue((content) => {
+ browser
+ .assert.ok(content.includes('"latestBlockNumber": "0x01"'), 'State is saved')
+ })
+ },
+
+ 'Should ensure that .states is not updated when save env option is unchecked #group5': function (browser: NightwatchBrowser) {
+ browser
+ .clickLaunchIcon('settings')
+ .waitForElementPresent('[data-id="settingsEnableSaveEnvStateLabel"]')
+ .click('[data-id="settingsEnableSaveEnvStateLabel"]')
+ .verify.elementNotPresent('[data-id="settingsEnableSaveEnvState"]:checked')
+ .clickLaunchIcon('filePanel')
+ .openFile('contracts/1_Storage.sol')
+ .pause(5000)
+ .clickLaunchIcon('udapp')
+ .waitForElementPresent('*[data-id="Deploy - transact (not payable)"]')
+ .click('*[data-id="Deploy - transact (not payable)"]')
+ .pause(5000)
+ .clickLaunchIcon('filePanel')
+ .openFile('.states/vm-shanghai/state.json')
+ .getEditorValue((content) => {
+ browser
+ .assert.ok(content.includes('"latestBlockNumber": "0x02"'), 'State is unchanged')
+ })
+ .end()
}
}
diff --git a/apps/remix-ide/src/app/tabs/locales/en/settings.json b/apps/remix-ide/src/app/tabs/locales/en/settings.json
index aa8f67449c..2ea6de0d4b 100644
--- a/apps/remix-ide/src/app/tabs/locales/en/settings.json
+++ b/apps/remix-ide/src/app/tabs/locales/en/settings.json
@@ -43,5 +43,6 @@
"settings.copilot": "Solidity copilot - Alpha",
"settings.copilot.activate": "Load & Activate copilot",
"settings.copilot.max_new_tokens": "Maximum number of words to generate",
- "settings.copilot.temperature": "Temperature"
+ "settings.copilot.temperature": "Temperature",
+ "settings.enableSaveEnvState": "Save environment state"
}
diff --git a/apps/remix-ide/src/app/tabs/locales/es/settings.json b/apps/remix-ide/src/app/tabs/locales/es/settings.json
index 899e2787cc..5d7ddda6ab 100644
--- a/apps/remix-ide/src/app/tabs/locales/es/settings.json
+++ b/apps/remix-ide/src/app/tabs/locales/es/settings.json
@@ -36,5 +36,6 @@
"settings.port": "PUERTO",
"settings.projectID": "ID DEL PROYECTO",
"settings.projectSecret": "SECRETO DE PROYECTO",
- "settings.analyticsInRemix": "Analíticas en IDE Remix"
+ "settings.analyticsInRemix": "Analíticas en IDE Remix",
+ "settings.enableSaveEnvState": "Save environment state"
}
diff --git a/apps/remix-ide/src/app/tabs/locales/fr/settings.json b/apps/remix-ide/src/app/tabs/locales/fr/settings.json
index 5d1859dfe9..3b61ab68c2 100644
--- a/apps/remix-ide/src/app/tabs/locales/fr/settings.json
+++ b/apps/remix-ide/src/app/tabs/locales/fr/settings.json
@@ -36,5 +36,6 @@
"settings.port": "PORT",
"settings.projectID": "ID du projet",
"settings.projectSecret": "SECRET DU PROJET",
- "settings.analyticsInRemix": "Analytics dans l'IDE de Remix"
+ "settings.analyticsInRemix": "Analytics dans l'IDE de Remix",
+ "settings.enableSaveEnvState": "Save environment state"
}
diff --git a/apps/remix-ide/src/app/tabs/locales/it/settings.json b/apps/remix-ide/src/app/tabs/locales/it/settings.json
index 416f338b64..251089f6cb 100644
--- a/apps/remix-ide/src/app/tabs/locales/it/settings.json
+++ b/apps/remix-ide/src/app/tabs/locales/it/settings.json
@@ -36,5 +36,6 @@
"settings.port": "PORTA",
"settings.projectID": "ID PROGETTO",
"settings.projectSecret": "SEGRETO DEL PROGETTO",
- "settings.analyticsInRemix": "Analytics nella Remix IDE"
+ "settings.analyticsInRemix": "Analytics nella Remix IDE",
+ "settings.enableSaveEnvState": "Save environment state"
}
diff --git a/apps/remix-ide/src/app/tabs/locales/zh/settings.json b/apps/remix-ide/src/app/tabs/locales/zh/settings.json
index 5504874efd..8a27f4a83d 100644
--- a/apps/remix-ide/src/app/tabs/locales/zh/settings.json
+++ b/apps/remix-ide/src/app/tabs/locales/zh/settings.json
@@ -36,5 +36,6 @@
"settings.port": "端口",
"settings.projectID": "项目 ID",
"settings.projectSecret": "项目密钥",
- "settings.analyticsInRemix": "Remix IDE 中的分析功能"
+ "settings.analyticsInRemix": "Remix IDE 中的分析功能",
+ "settings.enableSaveEnvState": "Save environment state"
}
diff --git a/apps/remix-ide/src/app/tabs/web3-provider.js b/apps/remix-ide/src/app/tabs/web3-provider.js
index 16c3a3dd17..d4efb04a77 100644
--- a/apps/remix-ide/src/app/tabs/web3-provider.js
+++ b/apps/remix-ide/src/app/tabs/web3-provider.js
@@ -63,6 +63,13 @@ export class Web3ProviderModule extends Plugin {
await this.call('compilerArtefacts', 'addResolvedContract', contractAddressStr, data)
}
}, 50)
+ const isVM = this.blockchain.executionContext.isVM()
+
+ if (isVM && this.blockchain.config.get('settings/save-evm-state')) {
+ await this.blockchain.executionContext.getStateDetails().then((state) => {
+ this.call('fileManager', 'writeFile', `.states/${this.blockchain.executionContext.getProvider()}/state.json`, state)
+ })
+ }
}
}
resolve(message)
diff --git a/apps/remix-ide/src/blockchain/blockchain.tsx b/apps/remix-ide/src/blockchain/blockchain.tsx
index fe9b6598f1..8095777a00 100644
--- a/apps/remix-ide/src/blockchain/blockchain.tsx
+++ b/apps/remix-ide/src/blockchain/blockchain.tsx
@@ -135,7 +135,8 @@ export class Blockchain extends Plugin {
setupEvents() {
this.executionContext.event.register('contextChanged', async (context) => {
- await this.resetEnvironment()
+ // 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}
@@ -643,8 +644,23 @@ export class Blockchain extends Plugin {
})
}
- async resetEnvironment() {
- await this.getCurrentProvider().resetEnvironment()
+ async loadContext(context: string) {
+ const saveEvmState = this.config.get('settings/save-evm-state')
+
+ if (saveEvmState) {
+ const contextExists = await this.call('fileManager', 'exists', `.states/${context}/state.json`)
+
+ if (contextExists) {
+ const stateDb = await this.call('fileManager', 'readFile', `.states/${context}/state.json`)
+
+ await this.getCurrentProvider().resetEnvironment(stateDb)
+ } else {
+ await this.getCurrentProvider().resetEnvironment()
+ }
+ } else {
+ await this.getCurrentProvider().resetEnvironment()
+ }
+
// TODO: most params here can be refactored away in txRunner
const web3Runner = new TxRunnerWeb3(
{
@@ -677,7 +693,7 @@ export class Blockchain extends Plugin {
view on etherscan
)
- }
+ }
})
})
this.txRunner = new TxRunner(web3Runner, {})
@@ -889,8 +905,13 @@ export class Blockchain extends Plugin {
let execResult
let returnValue = null
if (isVM) {
- const hhlogs = await this.web3().remix.getHHLogsForTx(txResult.transactionHash)
+ if (!tx.useCall && this.config.get('settings/save-evm-state')) {
+ 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)
if (hhlogs && hhlogs.length) {
const finalLogs = (
diff --git a/apps/remix-ide/src/blockchain/execution-context.js b/apps/remix-ide/src/blockchain/execution-context.js
index 7c33bb4f27..89ad383c7c 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() {
+ const db = await this.web3().remix.getStateDb()
+ const blocksData = await this.web3().remix.getBlocksData()
+ const state = {
+ db: Object.fromEntries(db._database),
+ blocks: blocksData.blocks,
+ latestBlockNumber: blocksData.latestBlockNumber
+ }
+ const stringifyed = JSON.stringify(state, (key, value) => {
+ if (key === 'db') {
+ return value
+ } else if (key === 'blocks') {
+ return value.map(block => bufferToHex(block))
+ }else if (key === '') {
+ return value
+ }
+ return bufferToHex(value)
+ }, '\t')
+
+ return stringifyed
+ }
}
diff --git a/apps/remix-ide/src/blockchain/providers/vm.ts b/apps/remix-ide/src/blockchain/providers/vm.ts
index 67357b347e..5156206c1c 100644
--- a/apps/remix-ide/src/blockchain/providers/vm.ts
+++ b/apps/remix-ide/src/blockchain/providers/vm.ts
@@ -2,6 +2,7 @@ import Web3, { FMT_BYTES, FMT_NUMBER, LegacySendAsyncProvider } from 'web3'
import { fromWei, toBigInt } from 'web3-utils'
import { privateToAddress, hashPersonalMessage, isHexString } from '@ethereumjs/util'
import { extend, JSONRPCRequestPayload, JSONRPCResponseCallback } from '@remix-project/remix-simulator'
+import {toBuffer} from '@ethereumjs/util'
import { ExecutionContext } from '../execution-context'
export class VMProvider {
@@ -12,9 +13,7 @@ export class VMProvider {
sendAsync: (query: JSONRPCRequestPayload, callback: JSONRPCResponseCallback) => void
}
newAccountCallback: {[stamp: number]: (error: Error, address: string) => void}
-
constructor (executionContext: ExecutionContext) {
-
this.executionContext = executionContext
this.worker = null
this.provider = null
@@ -29,7 +28,7 @@ export class VMProvider {
})
}
- async resetEnvironment () {
+ async resetEnvironment (stringifiedState?: string) {
if (this.worker) this.worker.terminate()
this.worker = new Worker(new URL('./worker-vm', import.meta.url))
const provider = this.executionContext.getProviderObject()
@@ -76,10 +75,35 @@ export class VMProvider {
}
}
})
- this.worker.postMessage({ cmd: 'init', fork: this.executionContext.getCurrentFork(), nodeUrl: provider?.options['nodeUrl'], blockNumber: provider?.options['blockNumber']})
+ if (stringifiedState) {
+ try {
+ const blockchainState = JSON.parse(stringifiedState)
+ const blockNumber = parseInt(blockchainState.latestBlockNumber, 16)
+ const stateDb = blockchainState.db
+
+ this.worker.postMessage({
+ cmd: 'init',
+ fork: this.executionContext.getCurrentFork(),
+ nodeUrl: provider?.options['nodeUrl'],
+ blockNumber,
+ stateDb,
+ blocks: blockchainState.blocks
+ })
+ } catch (e) {
+ console.error(e)
+ }
+ } else {
+ this.worker.postMessage({
+ cmd: 'init',
+ fork: this.executionContext.getCurrentFork(),
+ nodeUrl: provider?.options['nodeUrl'],
+ blockNumber: provider?.options['blockNumber']
+ })
+ }
})
}
+
// TODO: is still here because of the plugin API
// can be removed later when we update the API
createVMAccount (newAccount) {
diff --git a/apps/remix-ide/src/blockchain/providers/worker-vm.ts b/apps/remix-ide/src/blockchain/providers/worker-vm.ts
index 64a8d0255b..e91e30dfd4 100644
--- a/apps/remix-ide/src/blockchain/providers/worker-vm.ts
+++ b/apps/remix-ide/src/blockchain/providers/worker-vm.ts
@@ -6,7 +6,7 @@ self.onmessage = (e: MessageEvent) => {
switch (data.cmd) {
case 'init':
{
- provider = new Provider({ fork: data.fork, nodeUrl: data.nodeUrl, blockNumber: data.blockNumber })
+ provider = new Provider({ fork: data.fork, nodeUrl: data.nodeUrl, blockNumber: data.blockNumber, stateDb: data.stateDb, blocks: data.blocks})
provider.init().then(() => {
self.postMessage({
cmd: 'initiateResult',
diff --git a/libs/remix-lib/src/execution/logsManager.ts b/libs/remix-lib/src/execution/logsManager.ts
index 7d9bba2c6b..fbb0d1cf8c 100644
--- a/libs/remix-lib/src/execution/logsManager.ts
+++ b/libs/remix-lib/src/execution/logsManager.ts
@@ -21,6 +21,7 @@ export class LogsManager {
eachOf(block.transactions, (tx: any, i, next) => {
const txHash = '0x' + tx.hash().toString('hex')
web3.eth.getTransactionReceipt(txHash, (_error, receipt) => {
+ if (!receipt) return next()
for (const log of receipt.logs) {
this.oldLogs.push({ type: 'block', blockNumber, block, tx, log, txNumber: i, receipt })
const subscriptions = this.getSubscriptionsFor({ type: 'block', blockNumber, block, tx, log, receipt})
diff --git a/libs/remix-lib/src/execution/txRunnerVM.ts b/libs/remix-lib/src/execution/txRunnerVM.ts
index 7604bee0b2..74e42feb51 100644
--- a/libs/remix-lib/src/execution/txRunnerVM.ts
+++ b/libs/remix-lib/src/execution/txRunnerVM.ts
@@ -24,24 +24,23 @@ export class TxRunnerVM {
pendingTxs
vmaccounts
queusTxs
- blocks
+ blocks: Buffer[]
logsManager
commonContext
blockParentHash
nextNonceForCall: number
getVMObject: () => any
- constructor (vmaccounts, api, getVMObject, blockNumber) {
+ constructor (vmaccounts, api, getVMObject, blocks: Buffer[] = []) {
this.event = new EventManager()
this.logsManager = new LogsManager()
// has a default for now for backwards compatibility
this.getVMObject = getVMObject
this.commonContext = this.getVMObject().common
- this.blockNumber = blockNumber || 0
+ this.blockNumber = Array.isArray(blocks) ? blocks.length : 0 // TODO: this should be set to the fetched block number count
this.pendingTxs = {}
this.vmaccounts = vmaccounts
this.queusTxs = []
- this.blocks = []
/*
txHash is generated using the nonce,
in order to have unique transaction hash, we need to keep using different nonce (in case of a call)
@@ -51,7 +50,15 @@ export class TxRunnerVM {
this.nextNonceForCall = 0
const vm = this.getVMObject().vm
- this.blockParentHash = vm.blockchain.genesisBlock.hash()
+ if (Array.isArray(blocks) && (blocks || []).length > 0) {
+ const lastBlock = Block.fromRLPSerializedBlock(blocks[blocks.length - 1], { common: this.commonContext })
+
+ this.blockParentHash = lastBlock.hash()
+ this.blocks = blocks
+ } else {
+ this.blockParentHash = vm.blockchain.genesisBlock.hash()
+ this.blocks = [vm.blockchain.genesisBlock.serialize()]
+ }
}
execute (args: InternalTransaction, confirmationCb, gasEstimationForceSend, promptCb, callback: VMExecutionCallBack) {
@@ -106,8 +113,7 @@ export class TxRunnerVM {
const coinbases = ['0x0e9281e9c6a0808672eaba6bd1220e144c9bb07a', '0x8945a1288dc78a6d8952a92c77aee6730b414778', '0x94d76e24f818426ae84aa404140e8d5f60e10e7e']
const difficulties = [69762765929000, 70762765929000, 71762765929000]
const difficulty = this.commonContext.consensusType() === ConsensusType.ProofOfStake ? 0 : difficulties[this.blockNumber % difficulties.length]
-
- const blocknumber = this.blockNumber + 1
+ const blocknumber = this.blocks.length
const block = Block.fromBlockData({
header: {
timestamp: new Date().getTime() / 1000 | 0,
@@ -122,10 +128,13 @@ export class TxRunnerVM {
}, { common: this.commonContext, hardforkByBlockNumber: false, hardforkByTTD: undefined })
if (!useCall) {
- this.blockNumber = this.blockNumber + 1
+ this.blockNumber = blocknumber
this.blockParentHash = block.hash()
this.runBlockInVm(tx, block, (err, result) => {
- if (!err) this.getVMObject().vm.blockchain.putBlock(block)
+ if (!err) {
+ this.getVMObject().vm.blockchain.putBlock(block)
+ this.blocks.push(block.serialize())
+ }
callback(err, result)
})
} else {
diff --git a/libs/remix-simulator/src/methods/transactions.ts b/libs/remix-simulator/src/methods/transactions.ts
index 6018f9fc99..41e502fe06 100644
--- a/libs/remix-simulator/src/methods/transactions.ts
+++ b/libs/remix-simulator/src/methods/transactions.ts
@@ -4,7 +4,7 @@ import { processTx } from './txProcess'
import { execution } from '@remix-project/remix-lib'
import { ethers } from 'ethers'
import { VMexecutionResult } from '@remix-project/remix-lib'
-import { RunTxResult } from '@ethereumjs/vm'
+import { VMContext } from '../vm-context'
import { Log, EvmError } from '@ethereumjs/evm'
const TxRunnerVM = execution.TxRunnerVM
const TxRunner = execution.TxRunner
@@ -19,7 +19,7 @@ export type VMExecResult = {
}
export class Transactions {
- vmContext
+ vmContext: VMContext
accounts
tags
txRunnerVMInstance
@@ -32,7 +32,7 @@ export class Transactions {
this.tags = {}
}
- init (accounts, blockNumber) {
+ init (accounts, blocksData: Buffer[]) {
this.accounts = accounts
const api = {
logMessage: (msg) => {
@@ -55,11 +55,11 @@ export class Transactions {
}
}
- this.txRunnerVMInstance = new TxRunnerVM(accounts, api, _ => this.vmContext.vmObject(), blockNumber)
+ this.txRunnerVMInstance = new TxRunnerVM(accounts, api, _ => this.vmContext.vmObject(), blocksData)
this.txRunnerInstance = new TxRunner(this.txRunnerVMInstance, {})
this.txRunnerInstance.vmaccounts = accounts
}
-
+
methods () {
return {
eth_sendTransaction: this.eth_sendTransaction.bind(this),
@@ -74,7 +74,9 @@ export class Transactions {
eth_getExecutionResultFromSimulator: this.eth_getExecutionResultFromSimulator.bind(this),
eth_getHHLogsForTx: this.eth_getHHLogsForTx.bind(this),
eth_getHashFromTagBySimulator: this.eth_getHashFromTagBySimulator.bind(this),
- eth_registerCallId: this.eth_registerCallId.bind(this)
+ eth_registerCallId: this.eth_registerCallId.bind(this),
+ eth_getStateDb: this.eth_getStateDb.bind(this),
+ eth_getBlocksData: this.eth_getBlocksData.bind(this)
}
}
@@ -198,6 +200,17 @@ export class Transactions {
cb()
}
+ eth_getStateDb (_, cb) {
+ cb(null, this.vmContext.currentVm.stateManager.getDb())
+ }
+
+ eth_getBlocksData (_, cb) {
+ cb(null, {
+ blocks: this.txRunnerVMInstance.blocks,
+ latestBlockNumber: this.txRunnerVMInstance.blockNumber
+ })
+ }
+
eth_call (payload, cb) {
// from might be lowercased address (web3)
if (payload.params && payload.params.length > 0 && payload.params[0].from) {
diff --git a/libs/remix-simulator/src/provider.ts b/libs/remix-simulator/src/provider.ts
index 59066c0ab6..95b7cf6632 100644
--- a/libs/remix-simulator/src/provider.ts
+++ b/libs/remix-simulator/src/provider.ts
@@ -11,6 +11,7 @@ import { Transactions } from './methods/transactions'
import { Debug } from './methods/debug'
import { VMContext } from './vm-context'
import { Web3PluginBase } from 'web3'
+import { Block } from '@ethereumjs/block'
export interface JSONRPCRequestPayload {
params: any[];
@@ -27,8 +28,20 @@ export interface JSONRPCResponsePayload {
export type JSONRPCResponseCallback = (err: Error, result?: JSONRPCResponsePayload) => void
+export type State = Record
+
+export type ProviderOptions = {
+ fork?: string,
+ nodeUrl?: string,
+ blockNumber?: number | 'latest',
+ stateDb?: State,
+ logDetails?: boolean
+ blocks?: string[],
+ coinbase?: string
+}
+
export class Provider {
- options: Record
+ options: ProviderOptions
vmContext
Accounts
Transactions
@@ -37,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'))
+ this.vmContext = new VMContext(options['fork'], options['nodeUrl'], options['blockNumber'], options['stateDb'], options['blocks'])
this.Accounts = new Web3Accounts(this.vmContext)
this.Transactions = new Transactions(this.vmContext)
@@ -60,7 +73,7 @@ export class Provider {
this.pendingRequests = []
await this.vmContext.init()
await this.Accounts.resetAccounts()
- this.Transactions.init(this.Accounts.accounts, this.vmContext.blockNumber)
+ this.Transactions.init(this.Accounts.accounts, this.vmContext.serializedBlocks)
this.initialized = true
if (this.pendingRequests.length > 0) {
this.pendingRequests.map((req) => {
@@ -168,4 +181,18 @@ class Web3TestPlugin extends Web3PluginBase {
params: [id]
})
}
+
+ public getStateDb() {
+ return this.requestManager.send({
+ method: 'eth_getStateDb',
+ params: []
+ })
+ }
+
+ public getBlocksData() {
+ return this.requestManager.send({
+ method: 'eth_getBlocksData',
+ params: []
+ })
+ }
}
diff --git a/libs/remix-simulator/src/vm-context.ts b/libs/remix-simulator/src/vm-context.ts
index 0bf58c3ad8..9acf1128c1 100644
--- a/libs/remix-simulator/src/vm-context.ts
+++ b/libs/remix-simulator/src/vm-context.ts
@@ -2,7 +2,7 @@
'use strict'
import { Cache } from '@ethereumjs/statemanager/dist/cache'
import { hash } from '@remix-project/remix-lib'
-import { bufferToHex, Account, toBuffer, bufferToBigInt} from '@ethereumjs/util'
+import { bufferToHex, Account, toBuffer, bufferToBigInt, bigIntToHex } from '@ethereumjs/util'
import { keccak256 } from 'ethereum-cryptography/keccak'
import type { Address } from '@ethereumjs/util'
import { decode } from 'rlp'
@@ -13,7 +13,7 @@ import { VmProxy } from './VmProxy'
import { VM } from '@ethereumjs/vm'
import type { BigIntLike } from '@ethereumjs/util'
import { Common, ConsensusType } from '@ethereumjs/common'
-import { Trie } from '@ethereumjs/trie'
+import { Trie, MapDB } from '@ethereumjs/trie'
import { DefaultStateManager, StateManager, EthersStateManager, EthersStateManagerOpts } from '@ethereumjs/statemanager'
import { StorageDump } from '@ethereumjs/statemanager/dist/interface'
import { EVM } from '@ethereumjs/evm'
@@ -21,7 +21,7 @@ import { EEI } from '@ethereumjs/vm'
import { Blockchain } from '@ethereumjs/blockchain'
import { Block } from '@ethereumjs/block'
import { Transaction } from '@ethereumjs/tx'
-import { bigIntToHex } from '@ethereumjs/util'
+import { State } from './provider'
/**
* Options for constructing a {@link StateManager}.
@@ -50,6 +50,11 @@ class StateManagerCommonStorageDump extends DefaultStateManager {
this.keyHashes = {}
}
+ getDb () {
+ // @ts-ignore
+ return this._trie.database().db
+ }
+
putContractStorage (address, key, value) {
this.keyHashes[hash.keccak(key).toString('hex')] = bufferToHex(key)
return super.putContractStorage(address, key, value)
@@ -100,7 +105,6 @@ export interface CustomEthersStateManagerOpts {
class CustomEthersStateManager extends StateManagerCommonStorageDump {
private provider: ethers.providers.StaticJsonRpcProvider | ethers.providers.JsonRpcProvider
private blockTag: string
-
constructor(opts: CustomEthersStateManagerOpts) {
super(opts)
if (typeof opts.provider === 'string') {
@@ -258,7 +262,7 @@ class CustomEthersStateManager extends StateManagerCommonStorageDump {
export type CurrentVm = {
vm: VM,
web3vm: VmProxy,
- stateManager: StateManager,
+ stateManager: StateManagerCommonStorageDump,
common: Common
}
@@ -298,12 +302,16 @@ export class VMContext {
exeResults: Record
nodeUrl: string
blockNumber: number | 'latest'
+ stateDb: State
+ rawBlocks: string[]
+ serializedBlocks: Buffer[]
- constructor (fork?: string, nodeUrl?: string, blockNumber?: number | 'latest') {
+ constructor (fork?: string, nodeUrl?: string, blockNumber?: number | 'latest', stateDb?: State, blocksData?: string[]) {
this.blockGasLimitDefault = 4300000
this.blockGasLimit = this.blockGasLimitDefault
this.currentFork = fork || 'merge'
this.nodeUrl = nodeUrl
+ this.stateDb = stateDb
this.blockNumber = blockNumber
this.blocks = {}
this.latestBlockNumber = "0x0"
@@ -311,6 +319,8 @@ export class VMContext {
this.txByHash = {}
this.exeResults = {}
this.logsManager = new LogsManager()
+ this.rawBlocks = blocksData
+ this.serializedBlocks = []
}
async init () {
@@ -318,7 +328,7 @@ export class VMContext {
}
async createVm (hardfork) {
- let stateManager: StateManager
+ let stateManager: StateManagerCommonStorageDump
if (this.nodeUrl) {
let block = this.blockNumber
if (this.blockNumber === 'latest') {
@@ -335,15 +345,25 @@ export class VMContext {
blockTag: '0x' + this.blockNumber.toString(16)
})
}
+ } else{
+ const db = this.stateDb ? new Map(Object.entries(this.stateDb).map(([k, v]) => [k, toBuffer(v)])) : new Map()
+ const mapDb = new MapDB(db)
+ const trie = await Trie.create({ useKeyHashing: true, db: mapDb, useRootPersistence: true })
- } else
- stateManager = new StateManagerCommonStorageDump()
+ stateManager = new StateManagerCommonStorageDump({ trie })
+ }
const consensusType = hardfork === 'berlin' || hardfork === 'london' ? ConsensusType.ProofOfWork : ConsensusType.ProofOfStake
const difficulty = consensusType === ConsensusType.ProofOfStake ? 0 : 69762765929000
const common = new VMCommon({ chain: 'mainnet', hardfork })
- const genesisBlock: Block = Block.fromBlockData({
+ const blocks = (this.rawBlocks || []).map(block => {
+ const serializedBlock = toBuffer(block)
+
+ this.serializedBlocks.push(serializedBlock)
+ return Block.fromRLPSerializedBlock(serializedBlock, { common })
+ })
+ const genesisBlock: Block = blocks.length > 0 && (blocks[0] || {}).isGenesis ? blocks[0] : Block.fromBlockData({
header: {
timestamp: (new Date().getTime() / 1000 | 0),
number: 0,
@@ -352,7 +372,6 @@ export class VMContext {
gasLimit: 8000000
}
}, { common, hardforkByBlockNumber: false, hardforkByTTD: undefined })
-
const blockchain = await Blockchain.create({ common, validateBlocks: false, validateConsensus: false, genesisBlock })
const eei = new EEI(stateManager, common, blockchain)
const evm = new EVM({ common, eei, allowUnlimitedContractSize: true })
@@ -365,13 +384,17 @@ export class VMContext {
blockchain,
evm
})
-
// VmProxy and VMContext are very intricated.
// VmProxy is used to track the EVM execution (to listen on opcode execution, in order for instance to generate the VM trace)
const web3vm = new VmProxy(this)
web3vm.setVM(vm)
this.addBlock(genesisBlock, true)
- return { vm, web3vm, stateManager, common }
+ if (blocks.length > 0) blocks.splice(0, 1)
+ blocks.forEach(block => {
+ blockchain.putBlock(block)
+ this.addBlock(block, false, false, web3vm)
+ })
+ return { vm, web3vm, stateManager, common, blocks }
}
getCurrentFork () {
@@ -390,7 +413,7 @@ export class VMContext {
return this.currentVm
}
- addBlock (block: Block, genesis?: boolean, isCall?: boolean) {
+ addBlock (block: Block, genesis?: boolean, isCall?: boolean, web3vm?: VmProxy) {
let blockNumber = bigIntToHex(block.header.number)
if (blockNumber === '0x') {
blockNumber = '0x0'
@@ -400,7 +423,8 @@ export class VMContext {
this.blocks[blockNumber] = block
this.latestBlockNumber = blockNumber
- if (!isCall && !genesis) this.logsManager.checkBlock(blockNumber, block, this.web3())
+ if (!isCall && !genesis && web3vm) this.logsManager.checkBlock(blockNumber, block, web3vm)
+ if (!isCall && !genesis && !web3vm) this.logsManager.checkBlock(blockNumber, block, this.web3())
}
trackTx (txHash, block, tx) {
diff --git a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx
index 28c6ec1f6c..57f0cc7dd1 100644
--- a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx
+++ b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx
@@ -19,7 +19,8 @@ import {
saveIpfsSettingsToast,
useAutoCompletion,
useShowGasInEditor,
- useDisplayErrors
+ useDisplayErrors,
+ saveEnvState
} from './settingsAction'
import {initialState, toastInitialState, toastReducer, settingReducer} from './settingsReducer'
import {Toaster} from '@remix-ui/toaster' // eslint-disable-line
@@ -69,6 +70,9 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
const useShowGas = props.config.get('settings/show-gas')
if (useShowGas === null || useShowGas === undefined) useShowGasInEditor(props.config, true, dispatch)
+
+ const enableSaveEnvState = props.config.get('settings/save-evm-state')
+ if (enableSaveEnvState === null || enableSaveEnvState === undefined) saveEnvState(props.config, true, dispatch)
}
useEffect(() => initValue(), [resetState, props.config])
useEffect(() => initValue(), [])
@@ -200,6 +204,10 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
useDisplayErrors(props.config, event.target.checked, dispatch)
}
+ const onchangeSaveEnvState= (event) => {
+ saveEnvState(props.config, event.target.checked, dispatch)
+ }
+
const getTextClass = (key) => {
if (props.config.get(key)) {
return textDark
@@ -217,6 +225,7 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
const isAutoCompleteChecked = props.config.get('settings/auto-completion') || false
const isShowGasInEditorChecked = props.config.get('settings/show-gas') || false
const displayErrorsChecked = props.config.get('settings/display-errors') || false
+ const isSaveEvmStateChecked = props.config.get('settings/save-evm-state') || false
return (
@@ -333,6 +342,18 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
+
+
+
+
)
diff --git a/libs/remix-ui/settings/src/lib/settingsAction.ts b/libs/remix-ui/settings/src/lib/settingsAction.ts
index 6e59d25ffb..22110f8cb9 100644
--- a/libs/remix-ui/settings/src/lib/settingsAction.ts
+++ b/libs/remix-ui/settings/src/lib/settingsAction.ts
@@ -90,3 +90,8 @@ export const saveIpfsSettingsToast = (config, dispatch, ipfsURL, ipfsProtocol, i
config.set('settings/ipfs-project-secret', ipfsProjectSecret)
dispatch({ type: 'save', payload: { message: 'IPFS settings have been saved' } })
}
+
+export const saveEnvState = (config, checked, dispatch) => {
+ config.set('settings/save-evm-state', checked)
+ dispatch({ type: 'save-evm-state', payload: { isChecked: checked, textClass: checked ? textDark : textSecondary } })
+}
diff --git a/libs/remix-ui/settings/src/lib/settingsReducer.ts b/libs/remix-ui/settings/src/lib/settingsReducer.ts
index abad510431..cfd8572f9e 100644
--- a/libs/remix-ui/settings/src/lib/settingsReducer.ts
+++ b/libs/remix-ui/settings/src/lib/settingsReducer.ts
@@ -56,7 +56,12 @@ export const initialState = {
name: 'copilot/suggest/temperature',
value: 0.5,
textClass: textSecondary
- }
+ },
+ {
+ name: 'save-evm-state',
+ isChecked: false,
+ textClass: textSecondary
+ },
]
}
@@ -173,6 +178,17 @@ export const settingReducer = (state, action) => {
return {
...state
}
+
+ case 'save-evm-state':
+ state.elementState.map(element => {
+ if (element.name === 'save-evm-state') {
+ element.isChecked = action.payload.isChecked
+ element.textClass = action.payload.textClass
+ }
+ })
+ return {
+ ...state
+ }
default:
return initialState
}