diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index 72757d12c7..155ed107a6 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -499,6 +499,38 @@ impl RpcClient { })? } + pub fn get_multiple_accounts(&self, pubkeys: &[Pubkey]) -> ClientResult>> { + Ok(self + .get_multiple_accounts_with_commitment(pubkeys, CommitmentConfig::default())? + .value) + } + + pub fn get_multiple_accounts_with_commitment( + &self, + pubkeys: &[Pubkey], + commitment_config: CommitmentConfig, + ) -> RpcResult>> { + let config = RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + commitment: Some(commitment_config), + data_slice: None, + }; + let pubkeys: Vec<_> = pubkeys.iter().map(|pubkey| pubkey.to_string()).collect(); + let response = self.send(RpcRequest::GetMultipleAccounts, json!([[pubkeys], config]))?; + let Response { + context, + value: accounts, + } = serde_json::from_value::>>(response)?; + let accounts: Vec> = accounts + .iter() + .map(|rpc_account| rpc_account.decode()) + .collect(); + Ok(Response { + context, + value: accounts, + }) + } + pub fn get_account_data(&self, pubkey: &Pubkey) -> ClientResult> { Ok(self.get_account(pubkey)?.data) } diff --git a/client/src/rpc_request.rs b/client/src/rpc_request.rs index f27e70e2de..748f4c7788 100644 --- a/client/src/rpc_request.rs +++ b/client/src/rpc_request.rs @@ -28,6 +28,7 @@ pub enum RpcRequest { GetLargestAccounts, GetLeaderSchedule, GetMinimumBalanceForRentExemption, + GetMultipleAccounts, GetProgramAccounts, GetRecentBlockhash, GetSignatureStatuses, @@ -80,6 +81,7 @@ impl fmt::Display for RpcRequest { RpcRequest::GetLargestAccounts => "getLargestAccounts", RpcRequest::GetLeaderSchedule => "getLeaderSchedule", RpcRequest::GetMinimumBalanceForRentExemption => "getMinimumBalanceForRentExemption", + RpcRequest::GetMultipleAccounts => "getMultipleAccounts", RpcRequest::GetProgramAccounts => "getProgramAccounts", RpcRequest::GetRecentBlockhash => "getRecentBlockhash", RpcRequest::GetSignatureStatuses => "getSignatureStatuses", @@ -110,11 +112,12 @@ impl fmt::Display for RpcRequest { } } -pub const NUM_LARGEST_ACCOUNTS: usize = 20; pub const MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS: usize = 256; pub const MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS_SLOT_RANGE: u64 = 10_000; pub const MAX_GET_CONFIRMED_BLOCKS_RANGE: u64 = 500_000; pub const MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS2_LIMIT: usize = 1_000; +pub const MAX_MULTIPLE_ACCOUNTS: usize = 20; +pub const NUM_LARGEST_ACCOUNTS: usize = 20; // Validators that are this number of slots behind are considered delinquent pub const DELINQUENT_VALIDATOR_SLOT_DISTANCE: u64 = 128; diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 2aaf431d35..5af070c894 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -14,7 +14,7 @@ use solana_account_decoder::{ get_token_account_mint, spl_token_id_v2_0, spl_token_v2_0_native_mint, token_amount_to_ui_amount, UiTokenAmount, }, - UiAccount, UiAccountData, UiAccountEncoding, + UiAccount, UiAccountData, UiAccountEncoding, UiDataSliceConfig, }; use solana_client::{ rpc_config::*, @@ -23,7 +23,7 @@ use solana_client::{ TokenAccountsFilter, DELINQUENT_VALIDATOR_SLOT_DISTANCE, MAX_GET_CONFIRMED_BLOCKS_RANGE, MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS2_LIMIT, MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS_SLOT_RANGE, - MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS, NUM_LARGEST_ACCOUNTS, + MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS, MAX_MULTIPLE_ACCOUNTS, NUM_LARGEST_ACCOUNTS, }, rpc_response::Response as RpcResponse, rpc_response::*, @@ -242,34 +242,34 @@ impl JsonRpcRequestProcessor { let bank = self.bank(config.commitment); let encoding = config.encoding.unwrap_or(UiAccountEncoding::Binary); check_slice_and_encoding(&encoding, config.data_slice.is_some())?; - let mut response = None; - if let Some(account) = bank.get_account(pubkey) { - if account.owner == spl_token_id_v2_0() && encoding == UiAccountEncoding::JsonParsed { - response = Some(get_parsed_token_account(bank.clone(), pubkey, account)); - } else if (encoding == UiAccountEncoding::Binary - || encoding == UiAccountEncoding::Base58) - && account.data.len() > 128 - { - let message = "Encoded binary (base 58) data should be less than 128 bytes, please use Base64 encoding.".to_string(); - return Err(error::Error { - code: error::ErrorCode::InvalidRequest, - message, - data: None, - }); - } else { - response = Some(UiAccount::encode( - pubkey, - account, - encoding, - None, - config.data_slice, - )); - } - } + let response = get_encoded_account(&bank, pubkey, encoding, config.data_slice)?; Ok(new_response(&bank, response)) } + pub fn get_multiple_accounts( + &self, + pubkeys: Vec, + config: Option, + ) -> Result>>> { + let mut accounts: Vec> = vec![]; + + let config = config.unwrap_or_default(); + let bank = self.bank(config.commitment); + let encoding = config.encoding.unwrap_or(UiAccountEncoding::Base64); + check_slice_and_encoding(&encoding, config.data_slice.is_some())?; + + for pubkey in pubkeys { + let response_account = + get_encoded_account(&bank, &pubkey, encoding.clone(), config.data_slice)?; + accounts.push(response_account) + } + Ok(Response { + context: RpcResponseContext { slot: bank.slot() }, + value: accounts, + }) + } + pub fn get_minimum_balance_for_rent_exemption( &self, data_len: usize, @@ -1270,6 +1270,34 @@ fn check_slice_and_encoding(encoding: &UiAccountEncoding, data_slice_is_some: bo } } +fn get_encoded_account( + bank: &Arc, + pubkey: &Pubkey, + encoding: UiAccountEncoding, + data_slice: Option, +) -> Result> { + let mut response = None; + if let Some(account) = bank.get_account(pubkey) { + if account.owner == spl_token_id_v2_0() && encoding == UiAccountEncoding::JsonParsed { + response = Some(get_parsed_token_account(bank.clone(), pubkey, account)); + } else if (encoding == UiAccountEncoding::Binary || encoding == UiAccountEncoding::Base58) + && account.data.len() > 128 + { + let message = "Encoded binary (base 58) data should be less than 128 bytes, please use Base64 encoding.".to_string(); + return Err(error::Error { + code: error::ErrorCode::InvalidRequest, + message, + data: None, + }); + } else { + response = Some(UiAccount::encode( + pubkey, account, encoding, None, data_slice, + )); + } + } + Ok(response) +} + /// Use a set of filters to get an iterator of keyed program accounts from a bank fn get_filtered_program_accounts( bank: &Arc, @@ -1431,6 +1459,14 @@ pub trait RpcSol { config: Option, ) -> Result>>; + #[rpc(meta, name = "getMultipleAccounts")] + fn get_multiple_accounts( + &self, + meta: Self::Metadata, + pubkey_strs: Vec, + config: Option, + ) -> Result>>>; + #[rpc(meta, name = "getProgramAccounts")] fn get_program_accounts( &self, @@ -1766,6 +1802,29 @@ impl RpcSol for RpcSolImpl { meta.get_account_info(&pubkey, config) } + fn get_multiple_accounts( + &self, + meta: Self::Metadata, + pubkey_strs: Vec, + config: Option, + ) -> Result>>> { + debug!( + "get_multiple_accounts rpc request received: {:?}", + pubkey_strs.len() + ); + if pubkey_strs.len() > MAX_MULTIPLE_ACCOUNTS { + return Err(Error::invalid_params(format!( + "Too many inputs provided; max {}", + MAX_MULTIPLE_ACCOUNTS + ))); + } + let mut pubkeys: Vec = vec![]; + for pubkey_str in pubkey_strs { + pubkeys.push(verify_pubkey(pubkey_str)?); + } + meta.get_multiple_accounts(pubkeys, config) + } + fn get_minimum_balance_for_rent_exemption( &self, meta: Self::Metadata, @@ -3159,6 +3218,123 @@ pub mod tests { result["error"].as_object().unwrap(); } + #[test] + fn test_rpc_get_multiple_accounts() { + let bob_pubkey = Pubkey::new_rand(); + let RpcHandler { io, meta, bank, .. } = start_rpc_handler_with_tx(&bob_pubkey); + + let address = Pubkey::new(&[9; 32]); + let data = vec![1, 2, 3, 4, 5]; + let mut account = Account::new(42, 5, &Pubkey::default()); + account.data = data.clone(); + bank.store_account(&address, &account); + + let non_existent_address = Pubkey::new(&[8; 32]); + + // Test 3 accounts, one non-existent, and one with data + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"getMultipleAccounts","params":[["{}", "{}", "{}"]]}}"#, + bob_pubkey, non_existent_address, address, + ); + let res = io.handle_request_sync(&req, meta.clone()); + let expected = json!({ + "jsonrpc": "2.0", + "result": { + "context":{"slot":0}, + "value":[{ + "owner": "11111111111111111111111111111111", + "lamports": 20, + "data": ["", "base64"], + "executable": false, + "rentEpoch": 0 + }, + null, + { + "owner": "11111111111111111111111111111111", + "lamports": 42, + "data": [base64::encode(&data), "base64"], + "executable": false, + "rentEpoch": 0 + }], + }, + "id": 1, + }); + let expected: Response = + serde_json::from_value(expected).expect("expected response deserialization"); + let result: Response = serde_json::from_str(&res.expect("actual response")) + .expect("actual response deserialization"); + assert_eq!(expected, result); + + // Test config settings still work with multiple accounts + let req = format!( + r#"{{ + "jsonrpc":"2.0","id":1,"method":"getMultipleAccounts","params":[ + ["{}", "{}", "{}"], + {{"encoding":"base58"}} + ] + }}"#, + bob_pubkey, non_existent_address, address, + ); + let res = io.handle_request_sync(&req, meta.clone()); + let result: Value = serde_json::from_str(&res.expect("actual response")) + .expect("actual response deserialization"); + assert_eq!(result["result"]["value"].as_array().unwrap().len(), 3); + assert_eq!( + result["result"]["value"][2]["data"], + json!([bs58::encode(&data).into_string(), "base58"]), + ); + + let req = format!( + r#"{{ + "jsonrpc":"2.0","id":1,"method":"getMultipleAccounts","params":[ + ["{}", "{}", "{}"], + {{"encoding":"base64", "dataSlice": {{"length": 2, "offset": 1}}}} + ] + }}"#, + bob_pubkey, non_existent_address, address, + ); + let res = io.handle_request_sync(&req, meta.clone()); + let result: Value = serde_json::from_str(&res.expect("actual response")) + .expect("actual response deserialization"); + assert_eq!(result["result"]["value"].as_array().unwrap().len(), 3); + assert_eq!( + result["result"]["value"][2]["data"], + json!([base64::encode(&data[1..3]), "base64"]), + ); + + let req = format!( + r#"{{ + "jsonrpc":"2.0","id":1,"method":"getMultipleAccounts","params":[ + ["{}", "{}", "{}"], + {{"encoding":"binary", "dataSlice": {{"length": 2, "offset": 1}}}} + ] + }}"#, + bob_pubkey, non_existent_address, address, + ); + let res = io.handle_request_sync(&req, meta.clone()); + let result: Value = serde_json::from_str(&res.expect("actual response")) + .expect("actual response deserialization"); + assert_eq!(result["result"]["value"].as_array().unwrap().len(), 3); + assert_eq!( + result["result"]["value"][2]["data"], + bs58::encode(&data[1..3]).into_string(), + ); + + let req = format!( + r#"{{ + "jsonrpc":"2.0","id":1,"method":"getMultipleAccounts","params":[ + ["{}", "{}", "{}"], + {{"encoding":"jsonParsed", "dataSlice": {{"length": 2, "offset": 1}}}} + ] + }}"#, + bob_pubkey, non_existent_address, address, + ); + let res = io.handle_request_sync(&req, meta); + let result: Value = serde_json::from_str(&res.expect("actual response")) + .expect("actual response deserialization"); + result["error"].as_object().unwrap(); + } + #[test] fn test_rpc_get_program_accounts() { let bob = Keypair::new(); diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index fb396f7959..a454bafe78 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -39,6 +39,7 @@ To interact with a Solana node inside a JavaScript application, use the [solana- - [getLargestAccounts](jsonrpc-api.md#getlargestaccounts) - [getLeaderSchedule](jsonrpc-api.md#getleaderschedule) - [getMinimumBalanceForRentExemption](jsonrpc-api.md#getminimumbalanceforrentexemption) +- [getMultipleAccounts](jsonrpc-api.md#getmultipleaccounts) - [getProgramAccounts](jsonrpc-api.md#getprogramaccounts) - [getRecentBlockhash](jsonrpc-api.md#getrecentblockhash) - [getSignatureStatuses](jsonrpc-api.md#getsignaturestatuses) @@ -836,6 +837,49 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, " {"jsonrpc":"2.0","result":500,"id":1} ``` +### getMultipleAccounts + +Returns the account information for a list of Pubkeys + +#### Parameters: + +- `` - An array of Pubkeys to query, as base-58 encoded strings +- `` - (optional) Configuration object containing the following optional fields: + - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) + - `encoding: ` - encoding for Account data, either "base58" (*slow*), "base64", or jsonParsed". "base58" is limited to Account data of less than 128 bytes. "base64" will return base64 encoded data for Account data of any size. + Parsed-JSON encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If parsed-JSON is requested but a parser cannot be found, the field falls back to base64 encoding, detectable when the `data` field is type ``. **jsonParsed encoding is UNSTABLE** + - (optional) `dataSlice: ` - limit the returned account data using the provided `offset: ` and `length: ` fields; only available for "base58" or "base64" encoding. + +#### Results: + +The result will be an RpcResponse JSON object with `value` equal to: + +An array of: + +- `` - if the account at that Pubkey doesn't exist +- `` - otherwise, a JSON object containing: + - `lamports: `, number of lamports assigned to this account, as a u64 + - `owner: `, base-58 encoded Pubkey of the program this account has been assigned to + - `data: <[string, encoding]|object>`, data associated with the account, either as encoded binary data or JSON format `{: }`, depending on encoding parameter + - `executable: `, boolean indicating if the account contains a program \(and is strictly read-only\) + - `rentEpoch: `, the epoch at which this account will next owe rent, as u64 + +#### Example: + +```bash +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getMultipleAccounts", "params":[["vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg", "4fYNw3dojWmQ4dXtSGE9epjRGy9pFSx62YypT7avPYvA"],{"dataSlice":{"offset":0,"length":0}}]}' http://localhost:8899 + +// Result +{"jsonrpc":"2.0","result":{"context":{"slot":1},"value":[{"data":["AAAAAAEAAAACtzNsyJrW0g==","base64"],"executable":false,"lamports":1000000000,"owner":"11111111111111111111111111111111","rentEpoch":2}},{"data":["","base64"],"executable":false,"lamports":5000000000,"owner":"11111111111111111111111111111111","rentEpoch":2}}],"id":1} + +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getMultipleAccounts", "params":[["vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg", "4fYNw3dojWmQ4dXtSGE9epjRGy9pFSx62YypT7avPYvA"],{"encoding": "base58"}]}' http://localhost:8899 + +// Result +{"jsonrpc":"2.0","result":{"context":{"slot":1},"value":[{"data":["11116bv5nS2h3y12kD1yUKeMZvGcKLSjQgX6BeV7u1FrjeJcKfsHRTPuR3oZ1EioKtYGiYxpxMG5vpbZLsbcBYBEmZZcMKaSoGx9JZeAuWf","base58"],"executable":false,"lamports":1000000000,"owner":"11111111111111111111111111111111","rentEpoch":2}},{"data":["","base58"],"executable":false,"lamports":5000000000,"owner":"11111111111111111111111111111111","rentEpoch":2}}],"id":1} +``` + ### getProgramAccounts Returns all accounts owned by the provided program Pubkey