Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

Add getTokenLargestAccounts endpoint #11322

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -4511,7 +4575,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 @@ -4522,11 +4586,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,
}
]
);
}
}