From c8fa0b1b0bebf70e8e97e90d2d3e0ae40b2cff8a Mon Sep 17 00:00:00 2001 From: George W <140627974+grw-ms@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:23:32 +0200 Subject: [PATCH] feat: impl `zks_getAllAccountBalances` and `zks_getConfirmedTokens` (#198) --- SUPPORTED_APIS.md | 61 ++++++++- e2e-tests/test/zks-apis.test.ts | 27 +++- src/cache.rs | 27 ++++ src/fork.rs | 7 + src/http_fork_source.rs | 80 ++++++++++- src/zks.rs | 231 ++++++++++++++++++++++++++++++-- test_endpoints.http | 22 +++ 7 files changed, 439 insertions(+), 16 deletions(-) diff --git a/SUPPORTED_APIS.md b/SUPPORTED_APIS.md index 0b5db81f..58fad3c5 100644 --- a/SUPPORTED_APIS.md +++ b/SUPPORTED_APIS.md @@ -113,11 +113,11 @@ The `status` options are: | [`NETWORK`](#network-namespace) | [`net_listening`](#net_listening) | `SUPPORTED` | Returns `true` if the client is actively listening for network connections
_(hard-coded to `false`)_ | | [`ZKS`](#zks-namespace) | [`zks_estimateFee`](#zks_estimateFee) | `SUPPORTED` | Gets the Fee estimation data for a given Request | | `ZKS` | `zks_estimateGasL1ToL2` | `NOT IMPLEMENTED` | Estimate of the gas required for a L1 to L2 transaction | -| `ZKS` | `zks_getAllAccountBalances` | `NOT IMPLEMENTED` | Returns all balances for confirmed tokens given by an account address | +| [`ZKS`](#zks-namespace) | [`zks_getAllAccountBalances`](#zks_getallaccountbalances) | `SUPPORTED` | Returns all balances for confirmed tokens given by an account address | | [`ZKS`](#zks-namespace) | [`zks_getBridgeContracts`](#zks_getbridgecontracts) | `SUPPORTED` | Returns L1/L2 addresses of default bridges | | [`ZKS`](#zks-namespace) | [`zks_getBlockDetails`](#zks_getblockdetails) | `SUPPORTED` | Returns additional zkSync-specific information about the L2 block | | `ZKS` | `zks_getBytecodeByHash` | `NOT IMPLEMENTED` | Returns bytecode of a transaction given by its hash | -| `ZKS` | `zks_getConfirmedTokens` | `NOT IMPLEMENTED` | Returns [address, symbol, name, and decimal] information of all tokens within a range of ids given by parameters `from` and `limit` | +| [`ZKS`](#zks-namespace) | [`zks_getConfirmedTokens`](#zks_getconfirmedtokens) | `SUPPORTED` | Returns [address, symbol, name, and decimal] information of all tokens within a range of ids given by parameters `from` and `limit` | | `ZKS` | `zks_getL1BatchBlockRange` | `NOT IMPLEMENTED` | Returns the range of blocks contained within a batch given by batch number | | `ZKS` | `zks_getL1BatchDetails` | `NOT IMPLEMENTED` | Returns data pertaining to a given batch | | `ZKS` | `zks_getL2ToL1LogProof` | `NOT IMPLEMENTED` | Given a transaction hash, and an index of the L2 to L1 log produced within the transaction, it returns the proof for the corresponding L2 to L1 log | @@ -1864,3 +1864,60 @@ curl --request POST \ --header 'content-type: application/json' \ --data '{"jsonrpc": "2.0", "id": 1, "method": "zks_getRawBlockTransactions", "params": [ 140599 ]}' ``` + +### `zks_getConfirmedTokens` + +[source](src/zks.rs) + +Get list of the tokens supported by ZkSync Era. The tokens are returned in alphabetical order by their symbol. This means that the token id is its position in an alphabetically sorted array of tokens. + +#### Arguments + ++ `from: u32` - Offset of tokens ++ `limit: u8` - Limit of number of tokens to return + +#### Status + +`SUPPORTED` + +#### Example + +```bash +curl --request POST \ + --url http://localhost:8011/ \ + --header 'content-type: application/json' \ + --data '{ + "jsonrpc": "2.0", + "id": "1", + "method": "zks_getConfirmedTokens", + "params": [0, 100] +}' +``` + +### `zks_getAllAccountBalances` + +[source](src/zks.rs) + +Get all known balances for a given account. + +#### Arguments + ++ `address: Address` - The user address with balances to check. + +#### Status + +`SUPPORTED` + +#### Example + +```bash +curl --request POST \ + --url http://localhost:8011/ \ + --header 'content-type: application/json' \ + --data '{ + "jsonrpc": "2.0", + "id": "1", + "method": "zks_getAllAccountBalances", + "params": ["0x364d6D0333432C3Ac016Ca832fb8594A8cE43Ca6"] +}' +``` diff --git a/e2e-tests/test/zks-apis.test.ts b/e2e-tests/test/zks-apis.test.ts index 35770486..01cd989e 100644 --- a/e2e-tests/test/zks-apis.test.ts +++ b/e2e-tests/test/zks-apis.test.ts @@ -2,7 +2,7 @@ import { expect } from "chai"; import { deployContract, getTestProvider } from "../helpers/utils"; import { Wallet } from "zksync-web3"; import { RichAccounts } from "../helpers/constants"; -import { ethers } from "ethers"; +import { BigNumber, ethers } from "ethers"; import * as hre from "hardhat"; import { TransactionRequest } from "zksync-web3/build/src/types"; import { Deployer } from "@matterlabs/hardhat-zksync-deploy"; @@ -152,3 +152,28 @@ describe("zks_getRawBlockTransactions", function () { expect(txns[0]["execute"]["calldata"]).to.equal(receipt.data); }); }); + +describe("zks_getConfirmedTokens", function () { + it("Should return only Ether", async function () { + const tokens = await provider.send("zks_getConfirmedTokens", [0, 100]); + expect(tokens.length).to.equal(1); + expect(tokens[0].name).to.equal("Ether"); + }); +}); + +describe("zks_getAllAccountBalances", function () { + it("Should return balance of a rich account", async function () { + // Arrange + const account = RichAccounts[0].Account; + const expectedBalance = ethers.utils.parseEther("1000000000000"); // 1_000_000_000_000 ETH + const ethAddress = "0x000000000000000000000000000000000000800a"; + await provider.send("hardhat_setBalance", [account, expectedBalance._hex]); + + // Act + const balances = await provider.send("zks_getAllAccountBalances", [account]); + const ethBalance = BigNumber.from(balances[ethAddress]); + + // Assert + expect(ethBalance.eq(expectedBalance)).to.be.true; + }); +}); diff --git a/src/cache.rs b/src/cache.rs index 4e9fd2bc..713bc850 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -50,6 +50,7 @@ pub(crate) struct Cache { block_raw_transactions: FxHashMap>, transactions: FxHashMap, bridge_addresses: Option, + confirmed_tokens: FxHashMap<(u32, u8), Vec>, } impl Cache { @@ -158,6 +159,32 @@ impl Cache { self.block_raw_transactions.get(number) } + /// Returns the cached confirmed tokens. + pub(crate) fn get_confirmed_tokens( + &self, + from: u32, + limit: u8, + ) -> Option<&Vec> { + if matches!(self.config, CacheConfig::None) { + return None; + } + self.confirmed_tokens.get(&(from, limit)) + } + + /// Cache confirmed tokens + pub(crate) fn set_confirmed_tokens( + &mut self, + from: u32, + limit: u8, + confirmed_tokens: Vec, + ) { + if matches!(self.config, CacheConfig::None) { + return; + } + self.confirmed_tokens + .insert((from, limit), confirmed_tokens); + } + /// Cache the raw transactions for the provided block number. pub(crate) fn insert_block_raw_transactions( &mut self, diff --git a/src/fork.rs b/src/fork.rs index c0fe871e..04e27dc1 100644 --- a/src/fork.rs +++ b/src/fork.rs @@ -260,6 +260,13 @@ pub trait ForkSource { /// Returns addresses of the default bridge contracts. fn get_bridge_contracts(&self) -> eyre::Result; + + /// Returns confirmed tokens + fn get_confirmed_tokens( + &self, + from: u32, + limit: u8, + ) -> eyre::Result>; } /// Holds the information about the original chain. diff --git a/src/http_fork_source.rs b/src/http_fork_source.rs index c375bd16..6061db0b 100644 --- a/src/http_fork_source.rs +++ b/src/http_fork_source.rs @@ -1,19 +1,19 @@ use std::sync::RwLock; +use crate::{ + cache::{Cache, CacheConfig}, + fork::{block_on, ForkSource}, +}; use eyre::Context; use zksync_basic_types::{H256, U256}; use zksync_types::api::{BridgeAddresses, Transaction}; +use zksync_web3_decl::types::Token; use zksync_web3_decl::{ jsonrpsee::http_client::{HttpClient, HttpClientBuilder}, namespaces::{EthNamespaceClient, ZksNamespaceClient}, types::Index, }; -use crate::{ - cache::{Cache, CacheConfig}, - fork::{block_on, ForkSource}, -}; - #[derive(Debug)] /// Fork source that gets the data via HTTP requests. pub struct HttpForkSource { @@ -306,6 +306,37 @@ impl ForkSource for HttpForkSource { }) .wrap_err("fork http client failed") } + + /// Returns known token addresses + fn get_confirmed_tokens(&self, from: u32, limit: u8) -> eyre::Result> { + if let Some(confirmed_tokens) = self + .cache + .read() + .ok() + .and_then(|guard| guard.get_confirmed_tokens(from, limit).cloned()) + { + tracing::debug!("using cached confirmed_tokens"); + return Ok(confirmed_tokens); + }; + + let client = self.create_client(); + block_on(async move { client.get_confirmed_tokens(from, limit).await }) + .map(|confirmed_tokens| { + self.cache + .write() + .map(|mut guard| { + guard.set_confirmed_tokens(from, limit, confirmed_tokens.clone()) + }) + .unwrap_or_else(|err| { + tracing::warn!( + "failed writing to cache for 'set_confirmed_tokens': {:?}", + err + ) + }); + confirmed_tokens + }) + .wrap_err("fork http client failed") + } } #[cfg(test)] @@ -688,4 +719,43 @@ mod tests { .expect("failed fetching bridge addresses"); testing::assert_bridge_addresses_eq(&input_bridge_addresses, &actual_bridge_addresses); } + + #[test] + fn test_get_confirmed_tokens_is_cached() { + let mock_server = testing::MockServer::run(); + mock_server.expect( + serde_json::json!({ + "jsonrpc": "2.0", + "id": 0, + "method": "zks_getConfirmedTokens", + "params": [0, 100] + }), + serde_json::json!({ + "jsonrpc": "2.0", + "result": [ + { + "decimals": 18, + "l1Address": "0xbe9895146f7af43049ca1c1ae358b0541ea49704", + "l2Address": "0x75af292c1c9a37b3ea2e6041168b4e48875b9ed5", + "name": "Coinbase Wrapped Staked ETH", + "symbol": "cbETH" + } + ], + "id": 0 + }), + ); + + let fork_source = HttpForkSource::new(mock_server.url(), CacheConfig::Memory); + + let tokens = fork_source + .get_confirmed_tokens(0, 100) + .expect("failed fetching tokens"); + assert_eq!(tokens.len(), 1); + assert_eq!(tokens[0].symbol, "cbETH"); + + let tokens = fork_source + .get_confirmed_tokens(0, 100) + .expect("failed fetching tokens"); + assert_eq!(tokens.len(), 1); + } } diff --git a/src/zks.rs b/src/zks.rs index 65c2cd1d..49eaa599 100644 --- a/src/zks.rs +++ b/src/zks.rs @@ -1,8 +1,11 @@ -use std::sync::{Arc, RwLock}; +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; use bigdecimal::BigDecimal; use futures::FutureExt; -use zksync_basic_types::{Address, L1BatchNumber, MiniblockNumber, U256}; +use zksync_basic_types::{AccountTreeId, Address, L1BatchNumber, MiniblockNumber, U256}; use zksync_core::api_server::web3::backend_jsonrpc::{ error::{internal_error, into_jsrpc_error}, namespaces::zks::ZksNamespaceT, @@ -14,8 +17,10 @@ use zksync_types::{ TransactionDetails, TransactionStatus, TransactionVariant, }, fee::Fee, - ExecuteTransactionCommon, ProtocolVersionId, Transaction, + utils::storage_key_for_standard_token_balance, + ExecuteTransactionCommon, ProtocolVersionId, Transaction, L2_ETH_TOKEN_ADDRESS, }; +use zksync_utils::h256_to_u256; use zksync_web3_decl::{ error::Web3Error, types::{Filter, Log}, @@ -200,10 +205,35 @@ impl ZksNamespaceT fn get_confirmed_tokens( &self, - _from: u32, - _limit: u8, + from: u32, + limit: u8, ) -> jsonrpc_core::BoxFuture>> { - not_implemented("zks_getConfirmedTokens") + let inner = self.node.clone(); + Box::pin(async move { + let reader = inner + .read() + .map_err(|_| into_jsrpc_error(Web3Error::InternalError))?; + + let fork_storage_read = reader + .fork_storage + .inner + .read() + .expect("failed reading fork storage"); + + match fork_storage_read.fork.as_ref() { + Some(fork) => Ok(fork + .fork_source + .get_confirmed_tokens(from, limit) + .map_err(|_e| into_jsrpc_error(Web3Error::InternalError))?), + None => Ok(vec![zksync_web3_decl::types::Token { + l1_address: Address::zero(), + l2_address: L2_ETH_TOKEN_ADDRESS, + name: "Ether".to_string(), + symbol: "ETH".to_string(), + decimals: 18, + }]), + } + }) } fn get_token_price( @@ -241,13 +271,48 @@ impl ZksNamespaceT } } + /// Get all known balances for a given account. + /// + /// # Arguments + /// + /// * `address` - The user address with balances to check. + /// + /// # Returns + /// + /// A `BoxFuture` containing a `Result` with a (Token, Balance) map where account has non-zero value. fn get_all_account_balances( &self, - _address: zksync_basic_types::Address, + address: zksync_basic_types::Address, ) -> jsonrpc_core::BoxFuture< jsonrpc_core::Result>, > { - not_implemented("zks_getAllAccountBalances") + let inner = self.node.clone(); + Box::pin({ + self.get_confirmed_tokens(0, 100) + .then(move |tokens| async move { + let tokens = + tokens.map_err(|_err| into_jsrpc_error(Web3Error::InternalError))?; + + let mut writer = inner + .write() + .map_err(|_err| into_jsrpc_error(Web3Error::InternalError))?; + + let mut balances = HashMap::new(); + for token in tokens { + let balance_key = storage_key_for_standard_token_balance( + AccountTreeId::new(token.l2_address), + &address, + ); + + let balance = writer.fork_storage.read_value(&balance_key); + if !balance.is_zero() { + balances.insert(token.l2_address, h256_to_u256(balance)); + } + } + + Ok(balances) + }) + }) } fn get_l2_to_l1_msg_proof( @@ -507,6 +572,7 @@ mod tests { use zksync_basic_types::{Address, H160, H256}; use zksync_types::api::{self, Block, TransactionReceipt, TransactionVariant}; use zksync_types::transaction_request::CallRequest; + use zksync_utils::u256_to_h256; #[tokio::test] async fn test_estimate_fee() { @@ -1025,4 +1091,153 @@ mod tests { .expect("get transaction details"); assert_eq!(txns.len(), 1); } + + #[tokio::test] + async fn test_get_all_account_balances_empty() { + let node = InMemoryNode::::default(); + let namespace = ZkMockNamespaceImpl::new(node.get_inner()); + let balances = namespace + .get_all_account_balances(Address::zero()) + .await + .expect("get balances"); + assert!(balances.is_empty()); + } + + #[tokio::test] + async fn test_get_confirmed_tokens_eth() { + let node = InMemoryNode::::default(); + let namespace = ZkMockNamespaceImpl::new(node.get_inner()); + let balances = namespace + .get_confirmed_tokens(0, 100) + .await + .expect("get balances"); + assert_eq!(balances.len(), 1); + assert_eq!(&balances[0].name, "Ether"); + } + + #[tokio::test] + async fn test_get_all_account_balances_forked() { + let cbeth_address = Address::from_str("0x75af292c1c9a37b3ea2e6041168b4e48875b9ed5") + .expect("failed to parse address"); + let mock_server = testing::MockServer::run(); + mock_server.expect( + serde_json::json!({ + "jsonrpc": "2.0", + "id": 0, + "method": "zks_getBlockDetails", + "params": [1] + }), + serde_json::json!({ + "jsonrpc": "2.0", + "result": { + "baseSystemContractsHashes": { + "bootloader": "0x010008a5c30072f79f8e04f90b31f34e554279957e7e2bf85d3e9c7c1e0f834d", + "default_aa": "0x01000663d7941c097ba2631096508cf9ec7769ddd40e081fd81b0d04dc07ea0e" + }, + "commitTxHash": null, + "committedAt": null, + "executeTxHash": null, + "executedAt": null, + "l1BatchNumber": 0, + "l1GasPrice": 0, + "l1TxCount": 1, + "l2FairGasPrice": 250000000, + "l2TxCount": 0, + "number": 0, + "operatorAddress": "0x0000000000000000000000000000000000000000", + "protocolVersion": "Version16", + "proveTxHash": null, + "provenAt": null, + "rootHash": "0xdaa77426c30c02a43d9fba4e841a6556c524d47030762eb14dc4af897e605d9b", + "status": "verified", + "timestamp": 1000 + }, + "id": 0 + }), + ); + mock_server.expect( + serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getBlockByHash", + "params": ["0xdaa77426c30c02a43d9fba4e841a6556c524d47030762eb14dc4af897e605d9b", true] + }), + serde_json::json!({ + "jsonrpc": "2.0", + "result": { + "baseFeePerGas": "0x0", + "difficulty": "0x0", + "extraData": "0x", + "gasLimit": "0xffffffff", + "gasUsed": "0x0", + "hash": "0xdaa77426c30c02a43d9fba4e841a6556c524d47030762eb14dc4af897e605d9b", + "l1BatchNumber": "0x0", + "l1BatchTimestamp": null, + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "miner": "0x0000000000000000000000000000000000000000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0000000000000000", + "number": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "sealFields": [], + "sha3Uncles": "0x0000000000000000000000000000000000000000000000000000000000000000", + "size": "0x0", + "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "0x3e8", + "totalDifficulty": "0x0", + "transactions": [], + "transactionsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "uncles": [] + }, + "id": 1 + }), + ); + mock_server.expect( + serde_json::json!({ + "jsonrpc": "2.0", + "id": 0, + "method": "zks_getConfirmedTokens", + "params": [0, 100] + }), + serde_json::json!({ + "jsonrpc": "2.0", + "result": [ + { + "decimals": 18, + "l1Address": "0xbe9895146f7af43049ca1c1ae358b0541ea49704", + "l2Address": "0x75af292c1c9a37b3ea2e6041168b4e48875b9ed5", + "name": "Coinbase Wrapped Staked ETH", + "symbol": "cbETH" + } + ], + "id": 0 + }), + ); + + let node = InMemoryNode::::new( + Some(ForkDetails::from_network(&mock_server.url(), Some(1), CacheConfig::None).await), + None, + Default::default(), + ); + let namespace = ZkMockNamespaceImpl::new(node.get_inner()); + { + let inner = node.get_inner(); + let writer = inner.write().unwrap(); + let mut fork = writer.fork_storage.inner.write().unwrap(); + fork.raw_storage.set_value( + storage_key_for_standard_token_balance( + AccountTreeId::new(cbeth_address), + &Address::repeat_byte(0x1), + ), + u256_to_h256(U256::from(1337)), + ); + } + + let balances = namespace + .get_all_account_balances(Address::repeat_byte(0x1)) + .await + .expect("get balances"); + assert_eq!(balances.get(&cbeth_address).unwrap(), &U256::from(1337)); + } } diff --git a/test_endpoints.http b/test_endpoints.http index bc130ba2..3aee9747 100644 --- a/test_endpoints.http +++ b/test_endpoints.http @@ -729,6 +729,28 @@ content-type: application/json POST http://localhost:8011 content-type: application/json +{ + "jsonrpc": "2.0", + "id": "1", + "method": "zks_getConfirmedTokens", + "params": [0, 100] +} + +### +POST http://localhost:8011 +content-type: application/json + +{ + "jsonrpc": "2.0", + "id": "1", + "method": "zks_getAllAccountBalances", + "params": ["0x36615Cf349d7F6344891B1e7CA7C72883F5dc049"] +} + +### +POST http://localhost:8011 +content-type: application/json + { "jsonrpc": "2.0", "id": "1",