Rpc: add getMultipleAccounts endpoint (#12005)

* Add rpc endpoint to return the state of multiple accounts from the same bank

* Add docs

* Review comments: Dedupe account code, default to base64, add max const

* Add get_multiple_accounts to rpc-client
This commit is contained in:
Tyera Eulberg 2020-09-03 11:35:06 -06:00 committed by GitHub
parent b940da4040
commit b22de369b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 282 additions and 27 deletions

View File

@ -499,6 +499,38 @@ impl RpcClient {
})?
}
pub fn get_multiple_accounts(&self, pubkeys: &[Pubkey]) -> ClientResult<Vec<Option<Account>>> {
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<Vec<Option<Account>>> {
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<Option<UiAccount>>>(response)?;
let accounts: Vec<Option<Account>> = accounts
.iter()
.map(|rpc_account| rpc_account.decode())
.collect();
Ok(Response {
context,
value: accounts,
})
}
pub fn get_account_data(&self, pubkey: &Pubkey) -> ClientResult<Vec<u8>> {
Ok(self.get_account(pubkey)?.data)
}

View File

@ -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;

View File

@ -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<Pubkey>,
config: Option<RpcAccountInfoConfig>,
) -> Result<RpcResponse<Vec<Option<UiAccount>>>> {
let mut accounts: Vec<Option<UiAccount>> = 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<Bank>,
pubkey: &Pubkey,
encoding: UiAccountEncoding,
data_slice: Option<UiDataSliceConfig>,
) -> Result<Option<UiAccount>> {
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<Bank>,
@ -1431,6 +1459,14 @@ pub trait RpcSol {
config: Option<RpcAccountInfoConfig>,
) -> Result<RpcResponse<Option<UiAccount>>>;
#[rpc(meta, name = "getMultipleAccounts")]
fn get_multiple_accounts(
&self,
meta: Self::Metadata,
pubkey_strs: Vec<String>,
config: Option<RpcAccountInfoConfig>,
) -> Result<RpcResponse<Vec<Option<UiAccount>>>>;
#[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<String>,
config: Option<RpcAccountInfoConfig>,
) -> Result<RpcResponse<Vec<Option<UiAccount>>>> {
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<Pubkey> = 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();

View File

@ -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:
- `<array>` - An array of Pubkeys to query, as base-58 encoded strings
- `<object>` - (optional) Configuration object containing the following optional fields:
- (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment)
- `encoding: <string>` - 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 `<string>`. **jsonParsed encoding is UNSTABLE**
- (optional) `dataSlice: <object>` - limit the returned account data using the provided `offset: <usize>` and `length: <usize>` fields; only available for "base58" or "base64" encoding.
#### Results:
The result will be an RpcResponse JSON object with `value` equal to:
An array of:
- `<null>` - if the account at that Pubkey doesn't exist
- `<object>` - otherwise, a JSON object containing:
- `lamports: <u64>`, number of lamports assigned to this account, as a u64
- `owner: <string>`, 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 `{<program>: <state>}`, depending on encoding parameter
- `executable: <bool>`, boolean indicating if the account contains a program \(and is strictly read-only\)
- `rentEpoch: <u64>`, 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