use std::collections::HashMap;
use solana_account::Account;
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_pubkey::Pubkey;
use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id;
use spl_token_interface::native_mint;
use crate::TokenError;
#[derive(Debug, Clone)]
pub struct TokenAccountState {
pub owner: Pubkey,
pub mints: HashMap<Pubkey, MintAndAta>,
}
impl TokenAccountState {
#[must_use]
pub fn empty(owner: Pubkey) -> Self {
Self {
owner,
mints: HashMap::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct MintAndAta {
pub mint_account: Account,
pub ata_address: Pubkey,
pub ata_account: Option<Account>,
}
pub async fn fetch_token_account_state(
rpc: &RpcClient,
owner: Pubkey,
mints: &[Pubkey],
) -> Result<TokenAccountState, TokenError> {
if mints.is_empty() {
return Ok(TokenAccountState::empty(owner));
}
let mint_account_opts = rpc.get_multiple_accounts(mints).await?;
let mut mint_accounts = Vec::with_capacity(mints.len());
for (i, opt) in mint_account_opts.into_iter().enumerate() {
match (mints[i] == native_mint::ID, opt) {
(_, Some(account)) => mint_accounts.push(account),
(true, None) => mint_accounts.push(native_mint_account()),
(false, None) => return Err(TokenError::MintNotFound(mints[i])),
}
}
assemble_token_account_state(rpc, owner, mints, &mint_accounts).await
}
fn native_mint_account() -> Account {
use solana_program_pack::Pack;
use spl_token::state::Mint;
let mint = Mint {
mint_authority: spl_token::solana_program::program_option::COption::None,
supply: 0,
decimals: native_mint::DECIMALS,
is_initialized: true,
freeze_authority: spl_token::solana_program::program_option::COption::None,
};
let mut data = vec![0u8; Mint::LEN];
Mint::pack(mint, &mut data).expect("native mint account serialization is infallible");
Account {
lamports: 0,
data,
owner: spl_token::ID,
executable: false,
rent_epoch: 0,
}
}
pub async fn assemble_token_account_state(
rpc: &RpcClient,
owner: Pubkey,
mints: &[Pubkey],
mint_accounts: &[Account],
) -> Result<TokenAccountState, TokenError> {
if mints.len() != mint_accounts.len() {
return Err(TokenError::LengthMismatch {
mints: mints.len(),
mint_accounts: mint_accounts.len(),
});
}
if mints.is_empty() {
return Ok(TokenAccountState::empty(owner));
}
let ata_addresses: Vec<Pubkey> = mints
.iter()
.zip(mint_accounts.iter())
.map(|(mint, acc)| get_associated_token_address_with_program_id(&owner, mint, &acc.owner))
.collect();
let ata_account_opts = rpc.get_multiple_accounts(&ata_addresses).await?;
let mints_map = mints
.iter()
.zip(mint_accounts.iter())
.zip(ata_addresses.iter())
.zip(ata_account_opts)
.map(|(((mint, mint_account), ata_address), ata_account)| {
(
*mint,
MintAndAta {
mint_account: mint_account.clone(),
ata_address: *ata_address,
ata_account,
},
)
})
.collect();
Ok(TokenAccountState {
owner,
mints: mints_map,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn empty_mints_returns_empty_state_no_rpc() {
let rpc = RpcClient::new_mock("succeeds".to_string());
let owner = Pubkey::new_unique();
let state = assemble_token_account_state(&rpc, owner, &[], &[])
.await
.unwrap();
assert_eq!(state.owner, owner);
assert!(state.mints.is_empty());
}
#[tokio::test]
async fn length_mismatch_returns_error_no_rpc() {
let rpc = RpcClient::new_mock("succeeds".to_string());
let owner = Pubkey::new_unique();
let mints = vec![Pubkey::new_unique(), Pubkey::new_unique()];
let mint_accounts = vec![Account {
lamports: 0,
data: vec![],
owner: spl_token::ID,
executable: false,
rent_epoch: 0,
}];
let err = assemble_token_account_state(&rpc, owner, &mints, &mint_accounts)
.await
.unwrap_err();
match err {
TokenError::LengthMismatch {
mints: m,
mint_accounts: ma,
} => {
assert_eq!(m, 2);
assert_eq!(ma, 1);
}
_ => panic!("expected LengthMismatch, got {err:?}"),
}
}
#[test]
fn empty_state_constructor() {
let owner = Pubkey::new_unique();
let state = TokenAccountState::empty(owner);
assert_eq!(state.owner, owner);
assert!(state.mints.is_empty());
}
#[tokio::test]
async fn fetch_empty_mints_returns_empty_state_no_rpc() {
let rpc = RpcClient::new_mock("succeeds".to_string());
let owner = Pubkey::new_unique();
let state = fetch_token_account_state(&rpc, owner, &[]).await.unwrap();
assert_eq!(state.owner, owner);
assert!(state.mints.is_empty());
}
#[test]
fn native_mint_account_is_valid_classic_mint_owned_by_spl_token() {
use solana_program_pack::Pack;
use spl_token::state::Mint;
let account = native_mint_account();
assert_eq!(account.owner, spl_token::ID);
let mint = Mint::unpack(&account.data).unwrap();
assert!(mint.is_initialized);
assert_eq!(mint.decimals, native_mint::DECIMALS);
}
}