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:
parent
b940da4040
commit
b22de369b7
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
228
core/src/rpc.rs
228
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<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();
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue