diff --git a/eth-rpc/contracts/Flipper.sol b/eth-rpc/contracts/Flipper.sol index 51aaafc..3dc8e12 100644 --- a/eth-rpc/contracts/Flipper.sol +++ b/eth-rpc/contracts/Flipper.sol @@ -5,8 +5,12 @@ pragma solidity ^0.8.0; contract Flipper { bool public value; + constructor() { + value = true; + } + function flip() external { - value = !value; + value = !value; } function getValue() external view returns (bool) { diff --git a/eth-rpc/contracts/Tester.sol b/eth-rpc/contracts/Tester.sol new file mode 100644 index 0000000..48ded9b --- /dev/null +++ b/eth-rpc/contracts/Tester.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Flipper - Stores and toggles a boolean value +contract Tester { + uint256 public value; + string public name; + + event TesterDeployed(address indexed creator); + + constructor() payable { + emit TesterDeployed(msg.sender); + value = 42; + name = "Hello world"; + } + + function setValue(uint256 v) external { + value = v; + } + + function setName(string memory v) external { + name = v; + } + +} + diff --git a/eth-rpc/package.json b/eth-rpc/package.json index de7f694..e0b50c2 100644 --- a/eth-rpc/package.json +++ b/eth-rpc/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "build": "npx tsx src/build-contracts.ts", - "test": "vitest --test-timeout=30000", - "test-init": "vitest run --test-timeout=30000", + "test": "vitest --test-timeout=30000 --no-file-parallelism", + "test-init": "vitest run --test-timeout=30000 --no-file-parallelism", "prettier": "prettier --write .", "solhint": "solhint \"contracts/**/*.sol\"" }, diff --git a/eth-rpc/src/geth-diff.test.ts b/eth-rpc/src/geth-diff.test.ts index e809a01..19991dc 100644 --- a/eth-rpc/src/geth-diff.test.ts +++ b/eth-rpc/src/geth-diff.test.ts @@ -1,9 +1,15 @@ -import { jsonRpcErrors, getByteCode, visit, createEnv } from './util.ts' +import { + jsonRpcErrors, + getByteCode, + visit, + createEnv, + deployFactory, +} from './util.ts' import { afterEach, describe, expect, inject, test } from 'vitest' import fs from 'node:fs' import { fail } from 'node:assert' -import { encodeFunctionData, Hex, parseEther, decodeEventLog } from 'viem' +import { encodeFunctionData, parseEther, decodeEventLog } from 'viem' import { ErrorsAbi } from '../abi/Errors.ts' import { EventExampleAbi } from '../abi/EventExample.ts' import { TracingCallerAbi } from '../abi/TracingCaller.ts' @@ -15,78 +21,36 @@ afterEach(() => { const envs = await Promise.all(inject('envs').map(createEnv)) for (const env of envs) { - describe(`${env.serverWallet.chain.name}`, () => { - const getErrorTesterAddr = (() => { - let contractAddress: Hex = '0x' - return async () => { - if (contractAddress !== '0x') { - return contractAddress - } - const hash = await env.serverWallet.deployContract({ - abi: ErrorsAbi, - bytecode: getByteCode('Errors', env.evm), - }) - const deployReceipt = - await env.serverWallet.waitForTransactionReceipt({ hash }) - contractAddress = deployReceipt.contractAddress! - return contractAddress - } - })() + const [getErrorTesterAddr] = deployFactory(env, () => + env.serverWallet.deployContract({ + abi: ErrorsAbi, + bytecode: getByteCode('Errors', env.evm), + }) + ) - const getEventExampleAddr = (() => { - let contractAddress: Hex = '0x' - return async () => { - if (contractAddress !== '0x') { - return contractAddress - } - const hash = await env.serverWallet.deployContract({ - abi: EventExampleAbi, - bytecode: getByteCode('EventExample', env.evm), - }) - const deployReceipt = - await env.serverWallet.waitForTransactionReceipt({ hash }) - contractAddress = deployReceipt.contractAddress! - return contractAddress - } - })() - - const getTracingExampleAddrs = (() => { - let callerAddr: Hex = '0x' - let calleeAddr: Hex = '0x' - return async () => { - if (callerAddr !== '0x') { - return [callerAddr, calleeAddr] - } - calleeAddr = await (async () => { - const hash = await env.serverWallet.deployContract({ - abi: TracingCalleeAbi, - bytecode: getByteCode('TracingCallee', env.evm), - }) - const receipt = - await env.serverWallet.waitForTransactionReceipt({ - hash, - }) - return receipt.contractAddress! - })() - - callerAddr = await (async () => { - const hash = await env.serverWallet.deployContract({ - abi: TracingCallerAbi, - args: [calleeAddr], - bytecode: getByteCode('TracingCaller', env.evm), - value: parseEther('10'), - }) - const receipt = - await env.serverWallet.waitForTransactionReceipt({ - hash, - }) - return receipt.contractAddress! - })() - - return [callerAddr, calleeAddr] - } - })() + const [getEventExampleAddr] = deployFactory(env, async () => + env.serverWallet.deployContract({ + abi: EventExampleAbi, + bytecode: getByteCode('EventExample', env.evm), + }) + ) + const [getTracingCalleeAddr] = deployFactory(env, async () => + env.serverWallet.deployContract({ + abi: TracingCalleeAbi, + bytecode: getByteCode('TracingCallee', env.evm), + }) + ) + + const [getTracingCallerAddr] = deployFactory(env, async () => + env.serverWallet.deployContract({ + abi: TracingCallerAbi, + args: [await getTracingCalleeAddr()], + bytecode: getByteCode('TracingCaller', env.evm), + value: parseEther('10'), + }) + ) + describe(`${env.serverWallet.chain.name}`, () => { test('triggerAssertError', async () => { try { await env.accountWallet.readContract({ @@ -390,7 +354,8 @@ for (const env of envs) { }) test('tracing', async () => { - let [callerAddr, calleeAddr] = await getTracingExampleAddrs() + const calleeAddr = await getTracingCalleeAddr() + const callerAddr = await getTracingCallerAddr() const receipt = await (async () => { const { request } = await env.serverWallet.simulateContract({ diff --git a/eth-rpc/src/methods.test.ts b/eth-rpc/src/methods.test.ts new file mode 100644 index 0000000..c746a11 --- /dev/null +++ b/eth-rpc/src/methods.test.ts @@ -0,0 +1,241 @@ +import { + encodeFunctionData, + Hex, + hexToBigInt, + hexToNumber, + parseEther, +} from 'viem' +import { createEnv, deployFactory, getByteCode } from './util.ts' +import { describe, expect, inject, test } from 'vitest' +import { TesterAbi } from '../abi/Tester.ts' + +const envs = await Promise.all(inject('envs').map(createEnv)) + +for (const env of envs) { + const [getTesterAddr, getTesterReceipt] = deployFactory(env, async () => + env.serverWallet.deployContract({ + abi: TesterAbi, + bytecode: getByteCode('Tester', env.evm), + value: parseEther('2'), + }) + ) + describe(`${env.serverWallet.chain.name}`, () => { + test('eth_accounts works', async () => { + const addresses = await env.debugClient.request({ + method: 'eth_accounts', + }) + expect(addresses).toHaveLength(1) + }) + + test('eth_blockNumber works', async () => { + await getTesterReceipt() + const res = await env.debugClient.request({ + method: 'eth_blockNumber', + }) + expect(hexToBigInt(res)).toBeTruthy() + }) + + test('eth_call works', async () => { + const value = await env.serverWallet.readContract({ + address: await getTesterAddr(), + abi: TesterAbi, + functionName: 'value', + }) + expect(value).toEqual(42n) + }) + + test('eth_chainId works', async () => { + const res = await env.debugClient.request({ + method: 'eth_chainId', + }) + expect(hexToNumber(res)).toEqual(env.chain.id) + }) + + test('eth_estimateGas works', async () => { + const res = await env.serverWallet.estimateContractGas({ + address: await getTesterAddr(), + abi: TesterAbi, + functionName: 'setValue', + args: [43n], + }) + + expect(res).toBeTruthy() + }) + + test('eth_gasPrice works', async () => { + const res = await env.debugClient.request({ + method: 'eth_gasPrice', + }) + expect(hexToNumber(res)).toBeTruthy() + }) + + test('eth_getBalance works', async () => { + const balance = await env.serverWallet.getBalance({ + address: await getTesterAddr(), + }) + expect(balance).toEqual(parseEther('2')) + }) + + test('eth_getBlockByHash and eth_getBlockByNumber works', async () => { + const { blockNumber, blockHash } = await getTesterReceipt() + const by_number = await env.serverWallet.getBlock({ + blockNumber, + }) + expect(by_number).toBeTruthy() + + const by_hash = await env.serverWallet.getBlock({ + blockHash, + }) + expect(by_hash).toEqual(by_number) + }) + + test('eth_getBlockTransactionCountByHash and eth_getBlockTransactionCountByNumber works', async () => { + const { blockNumber, blockHash } = await getTesterReceipt() + const byNumber = await env.serverWallet.getBlockTransactionCount({ + blockNumber, + }) + + const byHash = await env.serverWallet.getBlockTransactionCount({ + blockHash, + }) + + expect(byNumber).toEqual(byHash) + expect(byNumber).toBeGreaterThanOrEqual(1) + }) + + test('eth_getCode works', async () => { + const address = await getTesterAddr() + const code = await env.serverWallet.getCode({ + address, + }) + + if (env.evm) { + expect(code).toBeTruthy() + } else { + expect(code).toEqual(getByteCode('Tester', env.evm)) + } + }) + + test('eth_getLogs works', async () => { + const { blockHash } = await getTesterReceipt() + const logs = await env.serverWallet.getLogs({ + blockHash, + }) + + expect(logs).toHaveLength(1) + }) + + test('eth_getStorageAt works', async () => { + const address = await getTesterAddr() + const storage = await env.serverWallet.getStorageAt({ + address, + slot: '0x01', + }) + + // revive store value as little endian. When this change in the compiler, or the runtime API, we can amend this test + if (env.evm) { + expect(storage).toEqual( + '0x48656c6c6f20776f726c64000000000000000000000000000000000000000016' + ) + } else { + expect(storage).toEqual( + '0x160000000000000000000000000000000000000000646c726f77206f6c6c6548' + ) + } + }) + + test('get_transaction_by_block_hash_and_index, eth_getTransactionByBlockNumberAndIndex and eth_getTransactionByHash works', async () => { + const { + transactionHash: hash, + blockHash, + transactionIndex: index, + blockNumber, + } = await getTesterReceipt() + const byTxHash = await env.serverWallet.getTransaction({ hash }) + expect(byTxHash).toBeTruthy() + const byBlockHash = await env.serverWallet.getTransaction({ + blockHash, + index, + }) + expect(byBlockHash).toEqual(byTxHash) + const byBlockNumber = await env.serverWallet.getTransaction({ + blockNumber, + index, + }) + expect(byBlockNumber).toEqual(byTxHash) + }) + + test('eth_getTransactionCount works', async () => { + const count = await env.serverWallet.getTransactionCount( + env.serverWallet.account + ) + expect(count).toBeGreaterThanOrEqual(1) + }) + + test('eth_getTransactionReceipt works', async () => { + const { transactionHash: hash } = await getTesterReceipt() + const receipt = await env.serverWallet.waitForTransactionReceipt({ + hash, + }) + expect(receipt).toBeTruthy() + }) + + test('eth_maxPriorityFeePerGas works', async () => { + const res: Hex = await env.serverWallet.request({ + method: 'eth_maxPriorityFeePerGas' as any, + }) + expect(hexToBigInt(res)).toBeTruthy() + }) + + test('eth_sendRawTransaction works', async () => { + const { request } = await env.accountWallet.simulateContract({ + address: await getTesterAddr(), + abi: TesterAbi, + functionName: 'setValue', + args: [42n], + }) + const hash = await env.accountWallet.writeContract(request) + let receipt = await env.serverWallet.waitForTransactionReceipt({ + hash, + }) + expect(receipt.status).toEqual('success') + }) + + test('eth_sendTransaction works', async () => { + const hash = await env.serverWallet.sendTransaction({ + to: await getTesterAddr(), + data: encodeFunctionData({ + abi: TesterAbi, + functionName: 'setValue', + args: [42n], + }), + }) + let receipt = await env.serverWallet.waitForTransactionReceipt({ + hash, + }) + expect(receipt.status).toEqual('success') + }) + + test('eth_syncing works', async () => { + const res = await env.serverWallet.request({ + method: 'eth_syncing', + }) + + expect(res).toEqual(false) + }) + + test('net_version works', async () => { + const res = await env.serverWallet.request({ + method: 'net_version' as any, + }) + expect(res).toBeTruthy() + }) + + test('web3_clientVersion works', async () => { + const res = await env.serverWallet.request({ + method: 'web3_clientVersion' as any, + }) + expect(res).toBeTruthy() + }) + }) +} diff --git a/eth-rpc/src/redstone.test.ts b/eth-rpc/src/redstone.test.ts index 269c991..54a21fb 100644 --- a/eth-rpc/src/redstone.test.ts +++ b/eth-rpc/src/redstone.test.ts @@ -1,5 +1,5 @@ import { ExampleRedstoneShowroomAbi } from '../abi/ExampleRedstoneShowroom.ts' -import { createEnv, getByteCode } from './util.ts' +import { createEnv, deployFactory, getByteCode } from './util.ts' import { Contract, providers } from 'ethers' import { WrapperBuilder } from '@redstone-finance/evm-connector' import { describe, expect, inject, test } from 'vitest' @@ -7,15 +7,6 @@ import { describe, expect, inject, test } from 'vitest' const envs = await Promise.all(inject('envs').map(createEnv)) for (const env of envs) { - const hash = await env.serverWallet.deployContract({ - abi: ExampleRedstoneShowroomAbi, - bytecode: getByteCode('ExampleRedstoneShowroom', env.evm), - }) - const deployReceipt = await env.serverWallet.waitForTransactionReceipt({ - hash, - }) - const contractAddress = deployReceipt.contractAddress! - const provider = new providers.JsonRpcProvider( env.chain.rpcUrls.default.http[0], { @@ -24,10 +15,17 @@ for (const env of envs) { } ) + const [getContractAddr] = deployFactory(env, () => + env.serverWallet.deployContract({ + abi: ExampleRedstoneShowroomAbi, + bytecode: getByteCode('ExampleRedstoneShowroom', env.evm), + }) + ) + describe(`${env.serverWallet.chain.name}`, () => { test('getTokensPrices works', async () => { const contract = new Contract( - contractAddress, + await getContractAddr(), ExampleRedstoneShowroomAbi as any, provider ) diff --git a/eth-rpc/src/test-setup.ts b/eth-rpc/src/test-setup.ts index 44aa000..668fde8 100644 --- a/eth-rpc/src/test-setup.ts +++ b/eth-rpc/src/test-setup.ts @@ -68,6 +68,10 @@ export default async function setup(project: TestProject) { ...(useEthRpc ? ['eth-rpc' as const] : []), ] + if (envs.length === 0) { + throw new Error('No environment started. Set USE_GETH or USE_ETH_RPC') + } + project.provide('envs', envs) return () => { diff --git a/eth-rpc/src/util.ts b/eth-rpc/src/util.ts index 48be314..8be7612 100644 --- a/eth-rpc/src/util.ts +++ b/eth-rpc/src/util.ts @@ -9,7 +9,9 @@ import { type Hex, hexToNumber, http, + parseEther, publicActions, + TransactionReceipt, } from 'viem' import { privateKeyToAccount, nonceManager } from 'viem/accounts' @@ -43,6 +45,7 @@ export function killProcessOnPort(port: number) { } export let jsonRpcErrors: JsonRpcError[] = [] +export type ChainEnv = Awaited> export async function createEnv(name: 'geth' | 'eth-rpc') { const gethPort = process.env.GETH_PORT ?? '8546' const ethRpcPort = process.env.ETH_RPC_PORT ?? '8545' @@ -110,6 +113,20 @@ export async function createEnv(name: 'geth' | 'eth-rpc') { chain, }).extend(publicActions) + // On geth let's endow the account wallet with some funds, to match the eth-rpc setup + if (name == 'geth') { + const endowment = parseEther('1000') + const balance = await serverWallet.getBalance(accountWallet.account) + if (balance < endowment / 2n) { + const hash = await serverWallet.sendTransaction({ + account: serverWallet.account, + to: accountWallet.account.address, + value: endowment, + }) + await serverWallet.waitForTransactionReceipt({ hash }) + } + } + const emptyWallet = createWalletClient({ account: privateKeyToAccount( '0x4450c571bae82da0528ecf76fcf7079e12ecc46dc873c9cacb6db8b75ed22f41', @@ -233,3 +250,30 @@ export function visit( return obj } } + +export function deployFactory(env: ChainEnv, deploy: () => Promise) { + return (() => { + let address: Hex | null = null + let receipt: TransactionReceipt | null = null + async function getAddress() { + if (address) { + return address + } + const hash = await deploy() + receipt = await env.serverWallet.waitForTransactionReceipt({ + hash, + }) + address = receipt.contractAddress! + return address + } + async function getReceipt() { + if (receipt) { + return receipt + } + await getAddress() + return receipt! + } + + return [getAddress, getReceipt] as const + })() +}