Skip to content

Commit

Permalink
Add getTokenLargestAccounts endpoint (#11322)
Browse files Browse the repository at this point in the history
  • Loading branch information
CriesofCarrots authored Aug 2, 2020
1 parent 27a8879 commit d1b2e6c
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 2 deletions.
7 changes: 7 additions & 0 deletions client/src/rpc_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,10 @@ pub struct RpcStakeActivation {
pub active: u64,
pub inactive: u64,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RpcTokenAccountBalance {
pub address: String,
pub amount: u64,
}
130 changes: 128 additions & 2 deletions core/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,48 @@ impl JsonRpcRequestProcessor {
Ok(new_response(&bank, supply))
}

pub fn get_token_largest_accounts(
&self,
mint: &Pubkey,
commitment: Option<CommitmentConfig>,
) -> Result<RpcResponse<Vec<RpcTokenAccountBalance>>> {
let bank = self.bank(commitment);
let mint_account = bank.get_account(mint).ok_or_else(|| {
Error::invalid_params("Invalid param: could not find mint".to_string())
})?;
if mint_account.owner != spl_token_id_v1_0() {
return Err(Error::invalid_params(
"Invalid param: not a v1.0 Token mint".to_string(),
));
}
let filters = vec![
// Filter on Mint address
RpcFilterType::Memcmp(Memcmp {
offset: 0,
bytes: MemcmpEncodedBytes::Binary(mint.to_string()),
encoding: None,
}),
// Filter on Token Account state
RpcFilterType::DataSize(size_of::<TokenAccount>() as u64),
];
let mut token_balances: Vec<RpcTokenAccountBalance> =
get_filtered_program_accounts(&bank, &mint_account.owner, filters)
.map(|(address, account)| {
let mut data = account.data.to_vec();
let amount = TokenState::unpack(&mut data)
.map(|account: &mut TokenAccount| account.amount)
.unwrap_or(0);
RpcTokenAccountBalance {
address: address.to_string(),
amount,
}
})
.collect();
token_balances.sort_by(|a, b| a.amount.cmp(&b.amount).reverse());
token_balances.truncate(NUM_LARGEST_ACCOUNTS);
Ok(new_response(&bank, token_balances))
}

pub fn get_token_accounts_by_owner(
&self,
owner: &Pubkey,
Expand Down Expand Up @@ -1346,6 +1388,14 @@ pub trait RpcSol {
commitment: Option<CommitmentConfig>,
) -> Result<RpcResponse<u64>>;

#[rpc(meta, name = "getTokenLargestAccounts")]
fn get_token_largest_accounts(
&self,
meta: Self::Metadata,
mint_str: String,
commitment: Option<CommitmentConfig>,
) -> Result<RpcResponse<Vec<RpcTokenAccountBalance>>>;

#[rpc(meta, name = "getTokenAccountsByOwner")]
fn get_token_accounts_by_owner(
&self,
Expand Down Expand Up @@ -1981,6 +2031,20 @@ impl RpcSol for RpcSolImpl {
meta.get_token_supply(&mint, commitment)
}

fn get_token_largest_accounts(
&self,
meta: Self::Metadata,
mint_str: String,
commitment: Option<CommitmentConfig>,
) -> Result<RpcResponse<Vec<RpcTokenAccountBalance>>> {
debug!(
"get_token_largest_accounts rpc request received: {:?}",
mint_str
);
let mint = verify_pubkey(mint_str)?;
meta.get_token_largest_accounts(&mint, commitment)
}

fn get_token_accounts_by_owner(
&self,
meta: Self::Metadata,
Expand Down Expand Up @@ -4514,7 +4578,7 @@ pub mod tests {
.expect("actual response deserialization");
assert!(result.get("error").is_some());

// Test non-existent Owner
// Test non-existent Delegate
let req = format!(
r#"{{
"jsonrpc":"2.0",
Expand All @@ -4525,11 +4589,73 @@ pub mod tests {
Pubkey::new_rand(),
spl_token_id_v1_0(),
);
let res = io.handle_request_sync(&req, meta);
let res = io.handle_request_sync(&req, meta.clone());
let result: Value = serde_json::from_str(&res.expect("actual response"))
.expect("actual response deserialization");
let accounts: Vec<RpcKeyedAccount> =
serde_json::from_value(result["result"]["value"].clone()).unwrap();
assert!(accounts.is_empty());

// Add new_mint, and another token account on new_mint with different balance
let mut mint_data = [0; size_of::<Mint>()];
let mint_state: &mut Mint = TokenState::unpack_unchecked(&mut mint_data).unwrap();
*mint_state = Mint {
owner: COption::Some(owner),
decimals: 2,
is_initialized: true,
};
let mint_account = Account {
lamports: 111,
data: mint_data.to_vec(),
owner: spl_token_id_v1_0(),
..Account::default()
};
bank.store_account(
&Pubkey::from_str(&new_mint.to_string()).unwrap(),
&mint_account,
);
let mut account_data = [0; size_of::<TokenAccount>()];
let account: &mut TokenAccount = TokenState::unpack_unchecked(&mut account_data).unwrap();
*account = TokenAccount {
mint: new_mint,
owner,
delegate: COption::Some(delegate),
amount: 10,
is_initialized: true,
is_native: false,
delegated_amount: 30,
};
let token_account = Account {
lamports: 111,
data: account_data.to_vec(),
owner: spl_token_id_v1_0(),
..Account::default()
};
let token_with_smaller_balance = Pubkey::new_rand();
bank.store_account(&token_with_smaller_balance, &token_account);

// Test largest token accounts
let req = format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"getTokenLargestAccounts","params":["{}"]}}"#,
new_mint,
);
let res = io.handle_request_sync(&req, meta);
let result: Value = serde_json::from_str(&res.expect("actual response"))
.expect("actual response deserialization");
let largest_accounts: Vec<RpcTokenAccountBalance> =
serde_json::from_value(result["result"]["value"].clone()).unwrap();
assert_eq!(
largest_accounts,
vec![
RpcTokenAccountBalance {
address: token_with_different_mint_pubkey.to_string(),
amount: 42,
},
RpcTokenAccountBalance {
address: token_with_smaller_balance.to_string(),
amount: 10,
}
]
);
}
}

0 comments on commit d1b2e6c

Please sign in to comment.