diff --git a/apps/remix-ide-e2e/src/tests/transactionExecution.test.ts b/apps/remix-ide-e2e/src/tests/transactionExecution.test.ts index 0dd61da685..a4ebf83709 100644 --- a/apps/remix-ide-e2e/src/tests/transactionExecution.test.ts +++ b/apps/remix-ide-e2e/src/tests/transactionExecution.test.ts @@ -249,6 +249,51 @@ module.exports = { browser.verifyCallReturnValue(addressRef, ['0:address: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045']) .perform(() => done()) }) + }, + + 'Should stay connected in the mainnet VM fork and execute state changing operations and non state changing operations #group5': function (browser: NightwatchBrowser) { + let addressRef + browser + .click('*[data-id="deployAndRunClearInstances"]') // clear udapp instances + .clickLaunchIcon('filePanel') + .testContracts('basic_state.sol', sources[9]['basic_state.sol'], ['BasicState']) + .clickLaunchIcon('udapp') + .selectContract('BasicState') + .createContract('') + .clickInstance(0) + .getAddressAtPosition(0, (address) => { + addressRef = address + }) + .clickFunction('cake - call') + .pause(500) + .perform((done) => { + browser.verifyCallReturnValue(addressRef, ['0:uint256: 0']) + .perform(() => done()) + }) + .clickFunction('up - transact (payable)') + .pause(500) + .clickFunction('cake - call') + .pause(1000) + .perform((done) => { + browser.verifyCallReturnValue(addressRef, ['0:uint256: 1']) + .perform(() => done()) + }) + .clickFunction('up - transact (payable)') + .pause(500) + .clickFunction('cake - call') + .pause(1000) + .perform((done) => { + browser.verifyCallReturnValue(addressRef, ['0:uint256: 2']) + .perform(() => done()) + }) + .clickFunction('up - transact (payable)') + .pause(500) + .clickFunction('cake - call') + .pause(1000) + .perform((done) => { + browser.verifyCallReturnValue(addressRef, ['0:uint256: 3']) + .perform(() => done()) + }) } } @@ -511,5 +556,18 @@ contract C { } ` } + }, + { + 'basic_state.sol': { + content: + ` + contract BasicState { + uint public cake; + function up() public payable { + cake++; + } + } + ` + } } ] diff --git a/apps/remix-ide/src/app/providers/goerli-vm-fork-provider.tsx b/apps/remix-ide/src/app/providers/goerli-vm-fork-provider.tsx index 8de9aa832a..9327f38226 100644 --- a/apps/remix-ide/src/app/providers/goerli-vm-fork-provider.tsx +++ b/apps/remix-ide/src/app/providers/goerli-vm-fork-provider.tsx @@ -14,7 +14,7 @@ export class GoerliForkVMProvider extends BasicVMProvider { version: packageJson.version }, blockchain) this.blockchain = blockchain - this.fork = 'merge' + this.fork = 'shanghai' this.nodeUrl = 'https://remix-sepolia.ethdevops.io' this.blockNumber = 'latest' } diff --git a/apps/remix-ide/src/app/providers/mainnet-vm-fork-provider.tsx b/apps/remix-ide/src/app/providers/mainnet-vm-fork-provider.tsx index a2ae20923c..68fc03e5ba 100644 --- a/apps/remix-ide/src/app/providers/mainnet-vm-fork-provider.tsx +++ b/apps/remix-ide/src/app/providers/mainnet-vm-fork-provider.tsx @@ -14,7 +14,7 @@ export class MainnetForkVMProvider extends BasicVMProvider { version: packageJson.version }, blockchain) this.blockchain = blockchain - this.fork = 'merge' + this.fork = 'shanghai' this.nodeUrl = 'https://mainnet.infura.io/v3/08b2a484451e4635a28b3d8234f24332' this.blockNumber = 'latest' } diff --git a/apps/remix-ide/src/app/providers/sepolia-vm-fork-provider.tsx b/apps/remix-ide/src/app/providers/sepolia-vm-fork-provider.tsx index 9f3fa374ae..042bd0ed7c 100644 --- a/apps/remix-ide/src/app/providers/sepolia-vm-fork-provider.tsx +++ b/apps/remix-ide/src/app/providers/sepolia-vm-fork-provider.tsx @@ -14,7 +14,7 @@ export class SepoliaForkVMProvider extends BasicVMProvider { version: packageJson.version }, blockchain) this.blockchain = blockchain - this.fork = 'merge' + this.fork = 'shanghai' this.nodeUrl = 'https://remix-sepolia.ethdevops.io' this.blockNumber = 'latest' } diff --git a/libs/remix-simulator/src/vm-context.ts b/libs/remix-simulator/src/vm-context.ts index be65caa43c..68544112e5 100644 --- a/libs/remix-simulator/src/vm-context.ts +++ b/libs/remix-simulator/src/vm-context.ts @@ -1,7 +1,9 @@ /* global ethereum */ 'use strict' +import { Cache } from '@ethereumjs/statemanager/dist/cache' import { hash } from '@remix-project/remix-lib' -import { bufferToHex } from '@ethereumjs/util' +import { bufferToHex, Account, toBuffer, bufferToBigInt} from '@ethereumjs/util' +import { keccak256 } from 'ethereum-cryptography/keccak' import type { Address } from '@ethereumjs/util' import { decode } from 'rlp' import { ethers } from 'ethers' @@ -38,44 +40,6 @@ export interface DefaultStateManagerOpts { prefixCodeHashes?: boolean } -class CustomEthersStateManager extends EthersStateManager { - keyHashes: { [key: string]: string } - constructor (opts: EthersStateManagerOpts) { - super(opts) - this.keyHashes = {} - } - - putContractStorage (address, key, value) { - this.keyHashes[bufferToHex(key).replace('0x', '')] = hash.keccak(key).toString('hex') - return super.putContractStorage(address, key, value) - } - - copy(): CustomEthersStateManager { - const newState = new CustomEthersStateManager({ - provider: (this as any).provider, - blockTag: BigInt((this as any).blockTag), - }) - ;(newState as any).contractCache = new Map((this as any).contractCache) - ;(newState as any).storageCache = new Map((this as any).storageCache) - ;(newState as any)._cache = this._cache - ;(newState as any).keyHashes = this.keyHashes - return newState - } - - async dumpStorage(address: Address): Promise { - const storageDump = {} - const storage = await super.dumpStorage(address) - for (const key of Object.keys(storage)) { - const value = storage[key] - storageDump['0x' + this.keyHashes[key]] = { - key: '0x' + key, - value: value - } - } - return storageDump - } -} - /* extend vm state manager and instanciate VM */ @@ -124,6 +88,158 @@ class StateManagerCommonStorageDump extends DefaultStateManager { } } +export interface CustomEthersStateManagerOpts { + provider: string | ethers.providers.StaticJsonRpcProvider | ethers.providers.JsonRpcProvider + blockTag: bigint | 'earliest', + /** + * A {@link Trie} instance + */ + trie?: Trie +} + +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') { + this.provider = new ethers.providers.StaticJsonRpcProvider(opts.provider) + } else if (opts.provider instanceof ethers.providers.JsonRpcProvider) { + this.provider = opts.provider + } else { + throw new Error(`valid JsonRpcProvider or url required; got ${opts.provider}`) + } + + this.blockTag = opts.blockTag === 'earliest' ? opts.blockTag : bigIntToHex(opts.blockTag) + + /* + * For a custom StateManager implementation adopt these + * callbacks passed to the `Cache` instantiated to perform + * the `get`, `put` and `delete` operations with the + * desired backend. + */ + const getCb = async (address) => { + const rlp = await this._trie.get(address.buf) + if (rlp) { + const ac = Account.fromRlpSerializedAccount(rlp) + return ac + } else { + const ac = await this.getAccountFromProvider(address) + return ac + } + } + const putCb = async (keyBuf, accountRlp) => { + const trie = this._trie + await trie.put(keyBuf, accountRlp) + } + const deleteCb = async (keyBuf: Buffer) => { + const trie = this._trie + await trie.del(keyBuf) + } + this._cache = new Cache({ getCb, putCb, deleteCb }) + } + + /** + * Sets the new block tag used when querying the provider and clears the + * internal cache. + * @param blockTag - the new block tag to use when querying the provider + */ + setBlockTag(blockTag: bigint | 'earliest'): void { + this.blockTag = blockTag === 'earliest' ? blockTag : bigIntToHex(blockTag) + } + + copy(): CustomEthersStateManager { + const newState = new CustomEthersStateManager({ + provider: this.provider, + blockTag: BigInt(this.blockTag), + trie: this._trie.copy(false), + }) + return newState + } + + /** + * Gets the code corresponding to the provided `address`. + * @param address - Address to get the `code` for + * @returns {Promise} - Resolves with the code corresponding to the provided address. + * Returns an empty `Buffer` if the account has no associated code. + */ + async getContractCode(address: Address): Promise { + const code = await super.getContractCode(address) + if (code && code.length > 0) return code + else { + const code = toBuffer(await this.provider.getCode(address.toString(), this.blockTag)) + await super.putContractCode(address, code) + return code + } + } + + /** + * Gets the storage value associated with the provided `address` and `key`. This method returns + * the shortest representation of the stored value. + * @param address - Address of the account to get the storage for + * @param key - Key in the account's storage to get the value for. Must be 32 bytes long. + * @returns {Promise} - The storage value for the account + * corresponding to the provided address at the provided key. + * If this does not exist an empty `Buffer` is returned. + */ + async getContractStorage(address: Address, key: Buffer): Promise { + let storage = await super.getContractStorage(address, key) + if (storage && storage.length > 0) return storage + else { + storage = toBuffer(await this.provider.getStorageAt( + address.toString(), + bufferToBigInt(key), + this.blockTag) + ) + await super.putContractStorage(address, key, storage) + return storage + } + } + + /** + * Checks if an `account` exists at `address` + * @param address - Address of the `account` to check + */ + async accountExists(address: Address): Promise { + const localAccount = this._cache.get(address) + if (!localAccount.isEmpty()) return true + // Get merkle proof for `address` from provider + const proof = await this.provider.send('eth_getProof', [address.toString(), [], this.blockTag]) + + const proofBuf = proof.accountProof.map((proofNode: string) => toBuffer(proofNode)) + + const trie = new Trie({ useKeyHashing: true }) + const verified = await trie.verifyProof( + Buffer.from(keccak256(proofBuf[0])), + address.buf, + proofBuf + ) + // if not verified (i.e. verifyProof returns null), account does not exist + return verified === null ? false : true + } + + /** + * Retrieves an account from the provider and stores in the local trie + * @param address Address of account to be retrieved from provider + * @private + */ + async getAccountFromProvider(address: Address): Promise { + const accountData = await this.provider.send('eth_getProof', [ + address.toString(), + [], + this.blockTag, + ]) + const account = Account.fromAccountData({ + balance: BigInt(accountData.balance), + nonce: BigInt(accountData.nonce), + codeHash: toBuffer(accountData.codeHash) + // storageRoot: toBuffer([]), // we have to remove this in order to force the creation of the Trie in the local state. + }) + return account + } +} + export type CurrentVm = { vm: VM, web3vm: VmProxy, diff --git a/libs/remix-ui/run-tab/src/lib/components/universalDappUI.tsx b/libs/remix-ui/run-tab/src/lib/components/universalDappUI.tsx index 0526d998ce..e00662e1bd 100644 --- a/libs/remix-ui/run-tab/src/lib/components/universalDappUI.tsx +++ b/libs/remix-ui/run-tab/src/lib/components/universalDappUI.tsx @@ -295,7 +295,7 @@ export function UniversalDappUI (props: UdappProps) { lookupOnly={lookupOnly} key={index} /> -
+ { lookupOnly &&
{Object.keys(props.instance.decodedResponse || {}).map( (key) => { @@ -319,7 +319,7 @@ export function UniversalDappUI (props: UdappProps) { } )} -
+
} ); })}