diff --git a/Cargo.lock b/Cargo.lock index 0550ca0c..e1d6b3e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1776,7 +1776,7 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "era_test_node" -version = "0.1.0-alpha.3" +version = "0.1.0-alpha.5" dependencies = [ "anyhow", "bigdecimal", diff --git a/Cargo.toml b/Cargo.toml index b8986232..86081cef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "era_test_node" -version = "0.1.0-alpha.3" +version = "0.1.0-alpha.5" edition = "2018" authors = ["The Matter Labs Team "] homepage = "https://zksync.io/" diff --git a/SUPPORTED_APIS.md b/SUPPORTED_APIS.md index f829cf16..df2c441d 100644 --- a/SUPPORTED_APIS.md +++ b/SUPPORTED_APIS.md @@ -25,7 +25,7 @@ The `status` options are: | `DEBUG` | `debug_traceBlockByHash` | `NOT IMPLEMENTED`
[GitHub Issue #63](https://github.com/matter-labs/era-test-node/issues/63) | Returns structured traces for operations within the block of the specified block hash | | `DEBUG` | `debug_traceBlockByNumber` | `NOT IMPLEMENTED`
[GitHub Issue #64](https://github.com/matter-labs/era-test-node/issues/64) | Returns structured traces for operations within the block of the specified block number | | `DEBUG` | `debug_traceTransaction` | `NOT IMPLEMENTED`
[GitHub Issue #65](https://github.com/matter-labs/era-test-node/issues/65) | Returns a structured trace of the execution of the specified transaction | -| `ETH` | `eth_accounts` | `NOT IMPLEMENTED`
[GitHub Issue #50](https://github.com/matter-labs/era-test-node/issues/50) | Returns a list of addresses owned by client | +| `ETH` | `eth_accounts` | `SUPPORTED` | Returns a list of addresses owned by client | | [`ETH`](#eth-namespace) | [`eth_chainId`](#eth_chainid) | `SUPPORTED` | Returns the currently configured chain id
_(default is `260`)_ | | `ETH` | `eth_coinbase` | `NOT IMPLEMENTED` | Returns the client coinbase address | | [`ETH`](#eth-namespace) | [`eth_estimateGas`](#eth_estimategas) | `SUPPORTED` | Generates and returns an estimate of how much gas is necessary for the transaction to complete | @@ -364,6 +364,29 @@ curl --request POST \ ## `ETH NAMESPACE` +### `eth_accounts` + +[source](src/node.rs) + +Returns a list of addresses owned by client + +#### Arguments + ++ _NONE_ + +#### Status + +`SUPPORTED` + +#### Example + +```bash +curl --request POST \ + --url http://localhost:8011/ \ + --header 'content-type: application/json' \ + --data '{"jsonrpc": "2.0","id": "1","method": "eth_accounts","params": []}' +``` + ### `eth_chainId` [source](src/node.rs) diff --git a/e2e-tests/helpers/constants.ts b/e2e-tests/helpers/constants.ts index 0a1ec928..8adca28d 100644 --- a/e2e-tests/helpers/constants.ts +++ b/e2e-tests/helpers/constants.ts @@ -1,42 +1,42 @@ -export const RichAccounts = { - 0: { +export const RichAccounts = [ + { Account: "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049", PrivateKey: "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110", }, - 1: { + { Account: "0xa61464658AfeAf65CccaaFD3a512b69A83B77618", PrivateKey: "0xac1e735be8536c6534bb4f17f06f6afc73b2b5ba84ac2cfb12f7461b20c0bbe3", }, - 2: { + { Account: "0x0D43eB5B8a47bA8900d84AA36656c92024e9772e", PrivateKey: "0xd293c684d884d56f8d6abd64fc76757d3664904e309a0645baf8522ab6366d9e", }, - 3: { + { Account: "0xA13c10C0D5bd6f79041B9835c63f91de35A15883", PrivateKey: "0x850683b40d4a740aa6e745f889a6fdc8327be76e122f5aba645a5b02d0248db8", }, - 4: { + { Account: "0x8002cD98Cfb563492A6fB3E7C8243b7B9Ad4cc92", PrivateKey: "0xf12e28c0eb1ef4ff90478f6805b68d63737b7f33abfa091601140805da450d93", }, - 5: { + { Account: "0x4F9133D1d3F50011A6859807C837bdCB31Aaab13", PrivateKey: "0xe667e57a9b8aaa6709e51ff7d093f1c5b73b63f9987e4ab4aa9a5c699e024ee8", }, - 6: { + { Account: "0xbd29A1B981925B94eEc5c4F1125AF02a2Ec4d1cA", PrivateKey: "0x28a574ab2de8a00364d5dd4b07c4f2f574ef7fcc2a86a197f65abaec836d1959", }, - 7: { + { Account: "0xedB6F5B4aab3dD95C7806Af42881FF12BE7e9daa", PrivateKey: "0x74d8b3a188f7260f67698eb44da07397a298df5427df681ef68c45b34b61f998", }, - 8: { + { Account: "0xe706e60ab5Dc512C36A4646D719b889F398cbBcB", PrivateKey: "0xbe79721778b48bcc679b78edac0ce48306a8578186ffcb9f2ee455ae6efeace1", }, - 9: { + { Account: "0xE90E12261CCb0F3F7976Ae611A29e84a6A85f424", PrivateKey: "0x3eb15da85647edd9a1159a4a13b9e7c56877c4eb33f614546d4db06a51868b1c", }, -} as const; +] as const; diff --git a/e2e-tests/test/eth-apis.test.ts b/e2e-tests/test/eth-apis.test.ts new file mode 100644 index 00000000..6c8d8e11 --- /dev/null +++ b/e2e-tests/test/eth-apis.test.ts @@ -0,0 +1,20 @@ +import { expect } from "chai"; +import { getTestProvider } from "../helpers/utils"; +import { RichAccounts } from "../helpers/constants"; +import { ethers } from "ethers"; + +const provider = getTestProvider(); + +describe("eth_accounts", function () { + it("Should return rich accounts", async function () { + // Arrange + const richAccounts = RichAccounts.map((ra) => ethers.utils.getAddress(ra.Account)).sort(); + + // Act + const response: string[] = await provider.send("eth_accounts", []); + const accounts = response.map((addr) => ethers.utils.getAddress(addr)).sort(); + + // Assert + expect(accounts).to.deep.equal(richAccounts); + }); +}); diff --git a/src/node.rs b/src/node.rs index e7476236..2503cad8 100644 --- a/src/node.rs +++ b/src/node.rs @@ -270,6 +270,7 @@ pub struct InMemoryNodeInner { pub console_log_handler: ConsoleLogHandler, pub system_contracts: SystemContracts, pub impersonated_accounts: HashSet
, + pub rich_accounts: HashSet, } type L2TxResult = ( @@ -712,6 +713,7 @@ impl InMemoryNode { console_log_handler: ConsoleLogHandler::default(), system_contracts: SystemContracts::from_options(system_contracts_options), impersonated_accounts: Default::default(), + rich_accounts: HashSet::new(), } } else { let mut block_hashes = HashMap::::new(); @@ -737,6 +739,7 @@ impl InMemoryNode { console_log_handler: ConsoleLogHandler::default(), system_contracts: SystemContracts::from_options(system_contracts_options), impersonated_accounts: Default::default(), + rich_accounts: HashSet::new(), } }; @@ -781,6 +784,7 @@ impl InMemoryNode { for (key, value) in keys.iter() { inner.fork_storage.set_value(*key, *value); } + inner.rich_accounts.insert(address); } /// Runs L2 'eth call' method - that doesn't commit to a block. @@ -1171,14 +1175,24 @@ impl InMemoryNode { // The computed block hash here will be different than that in production. let hash = compute_hash(batch_env.number.0, l2_tx.hash()); + + let mut transaction = zksync_types::api::Transaction::from(l2_tx); + let block_hash = inner + .block_hashes + .get(&inner.current_miniblock) + .ok_or(format!( + "Block hash not found for block: {}", + inner.current_miniblock + ))?; + transaction.block_hash = Some(*block_hash); + transaction.block_number = Some(U64::from(inner.current_miniblock)); + let block = Block { hash, number: U64::from(inner.current_miniblock.saturating_add(1)), timestamp: U256::from(batch_env.timestamp), l1_batch_number: Some(U64::from(batch_env.number.0)), - transactions: vec![TransactionVariant::Full( - zksync_types::api::Transaction::from(l2_tx), - )], + transactions: vec![TransactionVariant::Full(transaction)], gas_used: U256::from(tx_result.statistics.gas_used), gas_limit: U256::from(BLOCK_GAS_LIMIT), ..Default::default() @@ -1809,50 +1823,66 @@ impl EthNamespaceT for let reader = inner .read() .map_err(|_| into_jsrpc_error(Web3Error::InternalError))?; - let tx_result = reader.tx_results.get(&hash); - - Ok(tx_result.and_then(|TransactionResult { info, .. }| { - let input_data = info.tx.common_data.input.clone().or(None)?; - - let chain_id = info.tx.common_data.extract_chain_id().or(None)?; - - Some(zksync_types::api::Transaction { - hash, - nonce: U256::from(info.tx.common_data.nonce.0), - block_hash: Some(hash), - block_number: Some(U64::from(info.miniblock_number)), - transaction_index: Some(U64::from(1)), - from: Some(info.tx.initiator_account()), - to: Some(info.tx.recipient_account()), - value: info.tx.execute.value, - gas_price: Default::default(), - gas: Default::default(), - input: input_data.data.into(), - v: Some(chain_id.into()), - r: Some(U256::zero()), - s: Some(U256::zero()), - raw: None, - transaction_type: { - let tx_type = match info.tx.common_data.transaction_type { - zksync_types::l2::TransactionType::LegacyTransaction => 0, - zksync_types::l2::TransactionType::EIP2930Transaction => 1, - zksync_types::l2::TransactionType::EIP1559Transaction => 2, - zksync_types::l2::TransactionType::EIP712Transaction => 113, - zksync_types::l2::TransactionType::PriorityOpTransaction => 255, - zksync_types::l2::TransactionType::ProtocolUpgradeTransaction => 254, - }; - Some(tx_type.into()) - }, - access_list: None, - max_fee_per_gas: Some(info.tx.common_data.fee.max_fee_per_gas), - max_priority_fee_per_gas: Some( - info.tx.common_data.fee.max_priority_fee_per_gas, - ), - chain_id: chain_id.into(), - l1_batch_number: Some(U64::from(info.batch_number as u64)), - l1_batch_tx_index: None, + + let maybe_result = { + // try retrieving transaction from memory, and if unavailable subsequently from the fork + reader.tx_results.get(&hash).and_then(|TransactionResult { info, .. }| { + let input_data = info.tx.common_data.input.clone().or(None)?; + let chain_id = info.tx.common_data.extract_chain_id().or(None)?; + Some(zksync_types::api::Transaction { + hash, + nonce: U256::from(info.tx.common_data.nonce.0), + block_hash: Some(hash), + block_number: Some(U64::from(info.miniblock_number)), + transaction_index: Some(U64::from(1)), + from: Some(info.tx.initiator_account()), + to: Some(info.tx.recipient_account()), + value: info.tx.execute.value, + gas_price: Default::default(), + gas: Default::default(), + input: input_data.data.into(), + v: Some(chain_id.into()), + r: Some(U256::zero()), + s: Some(U256::zero()), + raw: None, + transaction_type: { + let tx_type = match info.tx.common_data.transaction_type { + zksync_types::l2::TransactionType::LegacyTransaction => 0, + zksync_types::l2::TransactionType::EIP2930Transaction => 1, + zksync_types::l2::TransactionType::EIP1559Transaction => 2, + zksync_types::l2::TransactionType::EIP712Transaction => 113, + zksync_types::l2::TransactionType::PriorityOpTransaction => 255, + zksync_types::l2::TransactionType::ProtocolUpgradeTransaction => 254, + }; + Some(tx_type.into()) + }, + access_list: None, + max_fee_per_gas: Some(info.tx.common_data.fee.max_fee_per_gas), + max_priority_fee_per_gas: Some( + info.tx.common_data.fee.max_priority_fee_per_gas, + ), + chain_id: chain_id.into(), + l1_batch_number: Some(U64::from(info.batch_number as u64)), + l1_batch_tx_index: None, + }) + }).or_else(|| { + reader + .fork_storage + .inner + .read() + .expect("failed reading fork storage") + .fork + .as_ref() + .and_then(|fork| { + fork.fork_source + .get_transaction_by_hash(hash) + .ok() + .flatten() + }) }) - })) + }; + + Ok(maybe_result) }) } @@ -2289,11 +2319,28 @@ impl EthNamespaceT for { Ok(zksync_basic_types::web3::types::SyncState::NotSyncing).into_boxed_future() } + /// Returns a list of available accounts. + /// + /// This function fetches the accounts from the inner state, and returns them as a list of addresses (`H160`). + /// + /// # Errors + /// + /// Returns a `jsonrpc_core::Result` error if acquiring a write lock on the inner state fails. + /// + /// # Returns + /// + /// A `BoxFuture` containing a `jsonrpc_core::Result` that resolves to a `Vec` of addresses. + fn accounts(&self) -> jsonrpc_core::BoxFuture>> { + let inner = Arc::clone(&self.inner); + let reader = match inner.read() { + Ok(r) => r, + Err(_) => { + return futures::future::err(into_jsrpc_error(Web3Error::InternalError)).boxed() + } + }; - fn accounts( - &self, - ) -> jsonrpc_core::BoxFuture>> { - not_implemented("accounts") + let accounts: Vec = reader.rich_accounts.clone().into_iter().collect(); + futures::future::ok(accounts).boxed() } fn coinbase( @@ -3322,4 +3369,25 @@ mod tests { .expect("failed getting filter changes"); assert_eq!(0, result.len()); } + + #[tokio::test] + async fn test_accounts() { + let node = InMemoryNode::::default(); + + let private_key = H256::repeat_byte(0x01); + let from_account = PackedEthSignature::address_from_private_key(&private_key).unwrap(); + node.set_rich_account(from_account); + + let account_result = node.accounts().await; + let expected_accounts: Vec = vec![from_account]; + + match account_result { + Ok(accounts) => { + assert_eq!(expected_accounts, accounts); + } + Err(e) => { + panic!("Failed to fetch accounts: {:?}", e); + } + } + } } diff --git a/test_endpoints.http b/test_endpoints.http index 13613b4c..30bd0f20 100644 --- a/test_endpoints.http +++ b/test_endpoints.http @@ -560,4 +560,15 @@ content-type: application/json "id": "1", "method": "eth_feeHistory", "params": ["0x1", "latest", [25, 50 , 75]] +} + +### +POST http://localhost:8011 +content-type: application/json + +{ + "jsonrpc": "2.0", + "id": "1", + "method": "eth_accounts", + "params": [] } \ No newline at end of file