diff --git a/Cargo.lock b/Cargo.lock index 523baf393..b13dbc515 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2931,7 +2931,6 @@ dependencies = [ "solana-sdk 1.2.13", "solana-sdk 1.3.0", "solana-vote-program", - "spl-memo", "spl-token", "thiserror", ] @@ -3237,6 +3236,7 @@ dependencies = [ "solana-perf", "solana-rayon-threadlimit", "solana-runtime", + "solana-sdk 1.2.13", "solana-sdk 1.3.0", "solana-sdk-macro-frozen-abi", "solana-stake-program", @@ -3246,6 +3246,7 @@ dependencies = [ "solana-version", "solana-vote-program", "solana-vote-signer", + "spl-token", "systemstat", "tempfile", "thiserror", diff --git a/account-decoder/Cargo.toml b/account-decoder/Cargo.toml index 4c4633c26..e5d24e69e 100644 --- a/account-decoder/Cargo.toml +++ b/account-decoder/Cargo.toml @@ -15,9 +15,8 @@ Inflector = "0.11.4" lazy_static = "1.4.0" solana-sdk = { path = "../sdk", version = "1.3.0" } solana-vote-program = { path = "../programs/vote", version = "1.3.0" } -spl-memo = { version = "1.0.4", features = ["skip-no-mangle"] } spl-sdk = { package = "solana-sdk", version = "=1.2.13", default-features = false } -spl-token = { version = "1.0.2", features = ["skip-no-mangle"] } +spl-token-v1-0 = { package = "spl-token", version = "1.0.2", features = ["skip-no-mangle"] } serde = "1.0.112" serde_derive = "1.0.103" serde_json = "1.0.56" diff --git a/account-decoder/src/parse_account_data.rs b/account-decoder/src/parse_account_data.rs index cde8500bd..07beeb7b9 100644 --- a/account-decoder/src/parse_account_data.rs +++ b/account-decoder/src/parse_account_data.rs @@ -1,16 +1,18 @@ -use crate::{parse_nonce::parse_nonce, parse_token::parse_token, parse_vote::parse_vote}; +use crate::{ + parse_nonce::parse_nonce, + parse_token::{parse_token, spl_token_id_v1_0}, + parse_vote::parse_vote, +}; use inflector::Inflector; use serde_json::{json, Value}; use solana_sdk::{instruction::InstructionError, pubkey::Pubkey, system_program}; -use std::{collections::HashMap, str::FromStr}; +use std::collections::HashMap; use thiserror::Error; lazy_static! { - static ref SYSTEM_PROGRAM_ID: Pubkey = - Pubkey::from_str(&system_program::id().to_string()).unwrap(); - static ref TOKEN_PROGRAM_ID: Pubkey = Pubkey::from_str(&spl_token::id().to_string()).unwrap(); - static ref VOTE_PROGRAM_ID: Pubkey = - Pubkey::from_str(&solana_vote_program::id().to_string()).unwrap(); + static ref SYSTEM_PROGRAM_ID: Pubkey = system_program::id(); + static ref TOKEN_PROGRAM_ID: Pubkey = spl_token_id_v1_0(); + static ref VOTE_PROGRAM_ID: Pubkey = solana_vote_program::id(); pub static ref PARSABLE_PROGRAM_IDS: HashMap = { let mut m = HashMap::new(); m.insert(*SYSTEM_PROGRAM_ID, ParsableAccount::Nonce); diff --git a/account-decoder/src/parse_token.rs b/account-decoder/src/parse_token.rs index bb2170d8d..cfb78ccea 100644 --- a/account-decoder/src/parse_token.rs +++ b/account-decoder/src/parse_token.rs @@ -1,10 +1,16 @@ use crate::parse_account_data::{ParsableAccount, ParseAccountError}; -use spl_sdk::pubkey::Pubkey; -use spl_token::{ +use solana_sdk::pubkey::Pubkey; +use spl_sdk::pubkey::Pubkey as SplPubkey; +use spl_token_v1_0::{ option::COption, state::{Account, Mint, Multisig, State}, }; -use std::mem::size_of; +use std::{mem::size_of, str::FromStr}; + +// A helper function to convert spl_token_v1_0::id() as spl_sdk::pubkey::Pubkey to solana_sdk::pubkey::Pubkey +pub fn spl_token_id_v1_0() -> Pubkey { + Pubkey::from_str(&spl_token_v1_0::id().to_string()).unwrap() +} pub fn parse_token(data: &[u8]) -> Result { let mut data = data.to_vec(); @@ -45,7 +51,7 @@ pub fn parse_token(data: &[u8]) -> Result { .signers .iter() .filter_map(|pubkey| { - if pubkey != &Pubkey::default() { + if pubkey != &SplPubkey::default() { Some(pubkey.to_string()) } else { None @@ -103,8 +109,8 @@ mod test { #[test] fn test_parse_token() { - let mint_pubkey = Pubkey::new(&[2; 32]); - let owner_pubkey = Pubkey::new(&[3; 32]); + let mint_pubkey = SplPubkey::new(&[2; 32]); + let owner_pubkey = SplPubkey::new(&[3; 32]); let mut account_data = [0; size_of::()]; let mut account: &mut Account = State::unpack_unchecked(&mut account_data).unwrap(); account.mint = mint_pubkey; @@ -138,12 +144,12 @@ mod test { }), ); - let signer1 = Pubkey::new(&[1; 32]); - let signer2 = Pubkey::new(&[2; 32]); - let signer3 = Pubkey::new(&[3; 32]); + let signer1 = SplPubkey::new(&[1; 32]); + let signer2 = SplPubkey::new(&[2; 32]); + let signer3 = SplPubkey::new(&[3; 32]); let mut multisig_data = [0; size_of::()]; let mut multisig: &mut Multisig = State::unpack_unchecked(&mut multisig_data).unwrap(); - let mut signers = [Pubkey::default(); 11]; + let mut signers = [SplPubkey::default(); 11]; signers[0] = signer1; signers[1] = signer2; signers[2] = signer3; diff --git a/client/src/rpc_config.rs b/client/src/rpc_config.rs index 6c8677c59..e10a3c1e3 100644 --- a/client/src/rpc_config.rs +++ b/client/src/rpc_config.rs @@ -58,3 +58,10 @@ pub struct RpcProgramAccountsConfig { #[serde(flatten)] pub account_config: RpcAccountInfoConfig, } + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum RpcTokenAccountsFilter { + Mint(String), + ProgramId(String), +} diff --git a/core/Cargo.toml b/core/Cargo.toml index 925248943..9ee1e14fe 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -67,6 +67,8 @@ solana-transaction-status = { path = "../transaction-status", version = "1.3.0" solana-version = { path = "../version", version = "1.3.0" } solana-vote-program = { path = "../programs/vote", version = "1.3.0" } solana-vote-signer = { path = "../vote-signer", version = "1.3.0" } +spl-sdk = { package = "solana-sdk", version = "=1.2.13", default-features = false } +spl-token-v1-0 = { package = "spl-token", version = "1.0.2", features = ["skip-no-mangle"] } tempfile = "3.1.0" thiserror = "1.0" tokio = "0.1" diff --git a/core/src/rpc.rs b/core/src/rpc.rs index e83082502..d32d0a996 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -8,10 +8,10 @@ use crate::{ use bincode::{config::Options, serialize}; use jsonrpc_core::{Error, Metadata, Result}; use jsonrpc_derive::rpc; -use solana_account_decoder::{UiAccount, UiAccountEncoding}; +use solana_account_decoder::{parse_token::spl_token_id_v1_0, UiAccount, UiAccountEncoding}; use solana_client::{ rpc_config::*, - rpc_filter::RpcFilterType, + rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}, rpc_request::{ DELINQUENT_VALIDATOR_SLOT_DISTANCE, MAX_GET_CONFIRMED_BLOCKS_RANGE, MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS_SLOT_RANGE, @@ -32,6 +32,7 @@ use solana_runtime::{ send_transaction_service::{SendTransactionService, TransactionInfo}, }; use solana_sdk::{ + account::Account, account_utils::StateMut, clock::{Slot, UnixTimestamp}, commitment_config::{CommitmentConfig, CommitmentLevel}, @@ -50,9 +51,11 @@ use solana_transaction_status::{ ConfirmedBlock, ConfirmedTransaction, TransactionStatus, UiTransactionEncoding, }; use solana_vote_program::vote_state::{VoteState, MAX_LOCKOUT_HISTORY}; +use spl_token_v1_0::state::{Account as TokenAccount, State as TokenState}; use std::{ cmp::{max, min}, collections::{HashMap, HashSet}, + mem::size_of, net::SocketAddr, rc::Rc, str::FromStr, @@ -249,14 +252,7 @@ impl JsonRpcRequestProcessor { let config = config.unwrap_or_default(); let bank = self.bank(config.commitment); let encoding = config.encoding.unwrap_or(UiAccountEncoding::Binary); - bank.get_program_accounts(Some(&program_id)) - .into_iter() - .filter(|(_, account)| { - filters.iter().all(|filter_type| match filter_type { - RpcFilterType::DataSize(size) => account.data.len() as u64 == *size, - RpcFilterType::Memcmp(compare) => compare.bytes_match(&account.data), - }) - }) + get_filtered_program_accounts(&bank, program_id, filters) .map(|(pubkey, account)| RpcKeyedAccount { pubkey: pubkey.to_string(), account: UiAccount::encode(account, encoding.clone()), @@ -839,6 +835,145 @@ impl JsonRpcRequestProcessor { inactive: inactive_stake, }) } + + pub fn get_token_account_balance( + &self, + pubkey: &Pubkey, + commitment: Option, + ) -> Result> { + let bank = self.bank(commitment); + let account = bank.get_account(pubkey).ok_or_else(|| { + Error::invalid_params("Invalid param: could not find account".to_string()) + })?; + + if account.owner != spl_token_id_v1_0() { + return Err(Error::invalid_params( + "Invalid param: not a v1.0 Token account".to_string(), + )); + } + let mut data = account.data.to_vec(); + let balance = TokenState::unpack(&mut data) + .map_err(|_| { + Error::invalid_params("Invalid param: not a v1.0 Token account".to_string()) + }) + .map(|account: &mut TokenAccount| account.amount)?; + Ok(new_response(&bank, balance)) + } + + pub fn get_token_supply( + &self, + mint: &Pubkey, + commitment: Option, + ) -> Result> { + 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::() as u64), + ]; + let supply = get_filtered_program_accounts(&bank, &mint_account.owner, filters) + .map(|(_pubkey, account)| { + let mut data = account.data.to_vec(); + TokenState::unpack(&mut data) + .map(|account: &mut TokenAccount| account.amount) + .unwrap_or(0) + }) + .sum(); + Ok(new_response(&bank, supply)) + } + + pub fn get_token_accounts_by_owner( + &self, + owner: &Pubkey, + token_account_filter: TokenAccountsFilter, + commitment: Option, + ) -> Result>> { + let bank = self.bank(commitment); + let (token_program_id, mint) = get_token_program_id_and_mint(&bank, token_account_filter)?; + + let mut filters = vec![ + // Filter on Owner address + RpcFilterType::Memcmp(Memcmp { + offset: 32, + bytes: MemcmpEncodedBytes::Binary(owner.to_string()), + encoding: None, + }), + // Filter on Token Account state + RpcFilterType::DataSize(size_of::() as u64), + ]; + if let Some(mint) = mint { + // Optional filter on Mint address + filters.push(RpcFilterType::Memcmp(Memcmp { + offset: 0, + bytes: MemcmpEncodedBytes::Binary(mint.to_string()), + encoding: None, + })); + } + let accounts = get_filtered_program_accounts(&bank, &token_program_id, filters) + .map(|(pubkey, account)| RpcKeyedAccount { + pubkey: pubkey.to_string(), + account: UiAccount::encode(account, UiAccountEncoding::JsonParsed), + }) + .collect(); + Ok(new_response(&bank, accounts)) + } + + pub fn get_token_accounts_by_delegate( + &self, + delegate: &Pubkey, + token_account_filter: TokenAccountsFilter, + commitment: Option, + ) -> Result>> { + let bank = self.bank(commitment); + let (token_program_id, mint) = get_token_program_id_and_mint(&bank, token_account_filter)?; + + let mut filters = vec![ + // Filter on Delegate is_some() + RpcFilterType::Memcmp(Memcmp { + offset: 72, + bytes: MemcmpEncodedBytes::Binary( + bs58::encode(bincode::serialize(&1u32).unwrap()).into_string(), + ), + encoding: None, + }), + // Filter on Delegate address + RpcFilterType::Memcmp(Memcmp { + offset: 76, + bytes: MemcmpEncodedBytes::Binary(delegate.to_string()), + encoding: None, + }), + // Filter on Token Account state + RpcFilterType::DataSize(size_of::() as u64), + ]; + if let Some(mint) = mint { + // Optional filter on Mint address + filters.push(RpcFilterType::Memcmp(Memcmp { + offset: 0, + bytes: MemcmpEncodedBytes::Binary(mint.to_string()), + encoding: None, + })); + } + let accounts = get_filtered_program_accounts(&bank, &token_program_id, filters) + .map(|(pubkey, account)| RpcKeyedAccount { + pubkey: pubkey.to_string(), + account: UiAccount::encode(account, UiAccountEncoding::JsonParsed), + }) + .collect(); + Ok(new_response(&bank, accounts)) + } } fn verify_filter(input: &RpcFilterType) -> Result<()> { @@ -859,6 +994,26 @@ fn verify_signature(input: &str) -> Result { .map_err(|e| Error::invalid_params(format!("Invalid param: {:?}", e))) } +pub enum TokenAccountsFilter { + Mint(Pubkey), + ProgramId(Pubkey), +} + +fn verify_token_account_filter( + token_account_filter: RpcTokenAccountsFilter, +) -> Result { + match token_account_filter { + RpcTokenAccountsFilter::Mint(mint_str) => { + let mint = verify_pubkey(mint_str)?; + Ok(TokenAccountsFilter::Mint(mint)) + } + RpcTokenAccountsFilter::ProgramId(program_id_str) => { + let program_id = verify_pubkey(program_id_str)?; + Ok(TokenAccountsFilter::ProgramId(program_id)) + } + } +} + /// Run transactions against a frozen bank without committing the results fn run_transaction_simulation( bank: &Bank, @@ -882,6 +1037,52 @@ fn run_transaction_simulation( ) } +/// Use a set of filters to get an iterator of keyed program accounts from a bank +fn get_filtered_program_accounts( + bank: &Arc, + program_id: &Pubkey, + filters: Vec, +) -> impl Iterator { + bank.get_program_accounts(Some(&program_id)) + .into_iter() + .filter(move |(_, account)| { + filters.iter().all(|filter_type| match filter_type { + RpcFilterType::DataSize(size) => account.data.len() as u64 == *size, + RpcFilterType::Memcmp(compare) => compare.bytes_match(&account.data), + }) + }) +} + +/// Analyze a passed Pubkey that may be a Token program id or Mint address to determine the program +/// id and optional Mint +fn get_token_program_id_and_mint( + bank: &Arc, + token_account_filter: TokenAccountsFilter, +) -> Result<(Pubkey, Option)> { + match token_account_filter { + TokenAccountsFilter::Mint(mint) => { + 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(), + )); + } + Ok((mint_account.owner, Some(mint))) + } + TokenAccountsFilter::ProgramId(program_id) => { + if program_id == spl_token_id_v1_0() { + Ok((program_id, None)) + } else { + Err(Error::invalid_params( + "Invalid param: unrecognized Token program id".to_string(), + )) + } + } + } +} + #[rpc] pub trait RpcSol { type Metadata; @@ -1154,6 +1355,44 @@ pub trait RpcSol { pubkey_str: String, config: Option, ) -> Result; + + // SPL Token-specific RPC endpoints + // See https://github.com/solana-labs/solana-program-library/releases/tag/token-v1.0.0 for + // program details + + #[rpc(meta, name = "getTokenAccountBalance")] + fn get_token_account_balance( + &self, + meta: Self::Metadata, + pubkey_str: String, + commitment: Option, + ) -> Result>; + + #[rpc(meta, name = "getTokenSupply")] + fn get_token_supply( + &self, + meta: Self::Metadata, + mint_str: String, + commitment: Option, + ) -> Result>; + + #[rpc(meta, name = "getTokenAccountsByOwner")] + fn get_token_accounts_by_owner( + &self, + meta: Self::Metadata, + owner_str: String, + token_account_filter: RpcTokenAccountsFilter, + commitment: Option, + ) -> Result>>; + + #[rpc(meta, name = "getTokenAccountsByDelegate")] + fn get_token_accounts_by_delegate( + &self, + meta: Self::Metadata, + delegate_str: String, + token_account_filter: RpcTokenAccountsFilter, + commitment: Option, + ) -> Result>>; } pub struct RpcSolImpl; @@ -1741,6 +1980,63 @@ impl RpcSol for RpcSolImpl { let pubkey = verify_pubkey(pubkey_str)?; meta.get_stake_activation(&pubkey, config) } + + fn get_token_account_balance( + &self, + meta: Self::Metadata, + pubkey_str: String, + commitment: Option, + ) -> Result> { + debug!( + "get_token_account_balance rpc request received: {:?}", + pubkey_str + ); + let pubkey = verify_pubkey(pubkey_str)?; + meta.get_token_account_balance(&pubkey, commitment) + } + + fn get_token_supply( + &self, + meta: Self::Metadata, + mint_str: String, + commitment: Option, + ) -> Result> { + debug!("get_token_supply rpc request received: {:?}", mint_str); + let mint = verify_pubkey(mint_str)?; + meta.get_token_supply(&mint, commitment) + } + + fn get_token_accounts_by_owner( + &self, + meta: Self::Metadata, + owner_str: String, + token_account_filter: RpcTokenAccountsFilter, + commitment: Option, + ) -> Result>> { + debug!( + "get_token_accounts_by_owner rpc request received: {:?}", + owner_str + ); + let owner = verify_pubkey(owner_str)?; + let token_account_filter = verify_token_account_filter(token_account_filter)?; + meta.get_token_accounts_by_owner(&owner, token_account_filter, commitment) + } + + fn get_token_accounts_by_delegate( + &self, + meta: Self::Metadata, + delegate_str: String, + token_account_filter: RpcTokenAccountsFilter, + commitment: Option, + ) -> Result>> { + debug!( + "get_token_accounts_by_delegate rpc request received: {:?}", + delegate_str + ); + let delegate = verify_pubkey(delegate_str)?; + let token_account_filter = verify_token_account_filter(token_account_filter)?; + meta.get_token_accounts_by_delegate(&delegate, token_account_filter, commitment) + } } fn deserialize_bs58_transaction(bs58_transaction: String) -> Result<(Vec, Transaction)> { @@ -1811,6 +2107,8 @@ pub mod tests { vote_instruction, vote_state::{Vote, VoteInit, MAX_LOCKOUT_HISTORY}, }; + use spl_sdk::pubkey::Pubkey as SplPubkey; + use spl_token_v1_0::{option::COption, state::Mint}; use std::collections::HashMap; const TEST_MINT_LAMPORTS: u64 = 1_000_000; @@ -3963,4 +4261,277 @@ pub mod tests { 3 )); } + + #[test] + fn test_token_rpcs() { + let RpcHandler { io, meta, bank, .. } = start_rpc_handler_with_tx(&Pubkey::new_rand()); + + let mut account_data = [0; size_of::()]; + let account: &mut TokenAccount = TokenState::unpack_unchecked(&mut account_data).unwrap(); + let mint = SplPubkey::new(&[2; 32]); + let owner = SplPubkey::new(&[3; 32]); + let delegate = SplPubkey::new(&[4; 32]); + *account = TokenAccount { + mint, + owner, + delegate: COption::Some(delegate), + amount: 42, + 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_account_pubkey = Pubkey::new_rand(); + bank.store_account(&token_account_pubkey, &token_account); + + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"getTokenAccountBalance","params":["{}"]}}"#, + token_account_pubkey, + ); + 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 balance: u64 = serde_json::from_value(result["result"]["value"].clone()).unwrap(); + assert_eq!(balance, 42); + + // Test non-existent token account + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"getTokenAccountBalance","params":["{}"]}}"#, + Pubkey::new_rand(), + ); + 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!(result.get("error").is_some()); + + // Add the mint, plus another token account to ensure getTokenSupply sums all mint accounts + let mut mint_data = [0; size_of::()]; + 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(&mint.to_string()).unwrap(), &mint_account); + let other_token_account_pubkey = Pubkey::new_rand(); + bank.store_account(&other_token_account_pubkey, &token_account); + + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"getTokenSupply","params":["{}"]}}"#, + mint, + ); + 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 supply: u64 = serde_json::from_value(result["result"]["value"].clone()).unwrap(); + assert_eq!(supply, 2 * 42); + + // Test non-existent mint address + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"getTokenSupply","params":["{}"]}}"#, + Pubkey::new_rand(), + ); + 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!(result.get("error").is_some()); + + // Add another token account with the same owner and delegate but different mint + let mut account_data = [0; size_of::()]; + let account: &mut TokenAccount = TokenState::unpack_unchecked(&mut account_data).unwrap(); + let new_mint = SplPubkey::new(&[5; 32]); + *account = TokenAccount { + mint: new_mint, + owner, + delegate: COption::Some(delegate), + amount: 42, + 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_different_mint_pubkey = Pubkey::new_rand(); + bank.store_account(&token_with_different_mint_pubkey, &token_account); + + // Test getTokenAccountsByOwner with Token program id returns all accounts, regardless of Mint address + let req = format!( + r#"{{ + "jsonrpc":"2.0", + "id":1, + "method":"getTokenAccountsByOwner", + "params":["{}", {{"programId": "{}"}}] + }}"#, + owner, + spl_token_id_v1_0(), + ); + 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 = + serde_json::from_value(result["result"]["value"].clone()).unwrap(); + assert_eq!(accounts.len(), 3); + + // Test returns only mint accounts + let req = format!( + r#"{{ + "jsonrpc":"2.0", + "id":1,"method":"getTokenAccountsByOwner", + "params":["{}", {{"mint": "{}"}}] + }}"#, + owner, mint, + ); + 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 = + serde_json::from_value(result["result"]["value"].clone()).unwrap(); + assert_eq!(accounts.len(), 2); + + // Test non-existent Mint/program id + let req = format!( + r#"{{ + "jsonrpc":"2.0", + "id":1, + "method":"getTokenAccountsByOwner", + "params":["{}", {{"programId": "{}"}}] + }}"#, + owner, + Pubkey::new_rand(), + ); + 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!(result.get("error").is_some()); + let req = format!( + r#"{{ + "jsonrpc":"2.0", + "id":1, + "method":"getTokenAccountsByOwner", + "params":["{}", {{"mint": "{}"}}] + }}"#, + owner, + Pubkey::new_rand(), + ); + 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!(result.get("error").is_some()); + + // Test non-existent Owner + let req = format!( + r#"{{ + "jsonrpc":"2.0", + "id":1, + "method":"getTokenAccountsByOwner", + "params":["{}", {{"programId": "{}"}}] + }}"#, + Pubkey::new_rand(), + spl_token_id_v1_0(), + ); + 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 = + serde_json::from_value(result["result"]["value"].clone()).unwrap(); + assert!(accounts.is_empty()); + + // Test getTokenAccountsByDelegate with Token program id returns all accounts, regardless of Mint address + let req = format!( + r#"{{ + "jsonrpc":"2.0", + "id":1, + "method":"getTokenAccountsByDelegate", + "params":["{}", {{"programId": "{}"}}] + }}"#, + delegate, + spl_token_id_v1_0(), + ); + 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 = + serde_json::from_value(result["result"]["value"].clone()).unwrap(); + assert_eq!(accounts.len(), 3); + + // Test returns only mint accounts + let req = format!( + r#"{{ + "jsonrpc":"2.0", + "id":1,"method": + "getTokenAccountsByDelegate", + "params":["{}", {{"mint": "{}"}}] + }}"#, + delegate, mint, + ); + 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 = + serde_json::from_value(result["result"]["value"].clone()).unwrap(); + assert_eq!(accounts.len(), 2); + + // Test non-existent Mint/program id + let req = format!( + r#"{{ + "jsonrpc":"2.0", + "id":1, + "method":"getTokenAccountsByDelegate", + "params":["{}", {{"programId": "{}"}}] + }}"#, + delegate, + Pubkey::new_rand(), + ); + 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!(result.get("error").is_some()); + let req = format!( + r#"{{ + "jsonrpc":"2.0", + "id":1, + "method":"getTokenAccountsByDelegate", + "params":["{}", {{"mint": "{}"}}] + }}"#, + delegate, + Pubkey::new_rand(), + ); + 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!(result.get("error").is_some()); + + // Test non-existent Owner + let req = format!( + r#"{{ + "jsonrpc":"2.0", + "id":1, + "method":"getTokenAccountsByDelegate", + "params":["{}", {{"programId": "{}"}}] + }}"#, + Pubkey::new_rand(), + spl_token_id_v1_0(), + ); + let res = io.handle_request_sync(&req, meta); + let result: Value = serde_json::from_str(&res.expect("actual response")) + .expect("actual response deserialization"); + let accounts: Vec = + serde_json::from_value(result["result"]["value"].clone()).unwrap(); + assert!(accounts.is_empty()); + } } diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index 5ca73c195..bce2172d2 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -45,6 +45,10 @@ To interact with a Solana node inside a JavaScript application, use the [solana- - [getSlotLeader](jsonrpc-api.md#getslotleader) - [getStakeActivation](jsonrpc-api.md#getstakeactivation) - [getSupply](jsonrpc-api.md#getsupply) +- [getTokenAccountBalance](jsonrpc-api.md#gettokenaccountbalance) +- [getTokenAccountsByDelegate](jsonrpc-api.md#gettokenaccountsbydelegate) +- [getTokenAccountsByOwner](jsonrpc-api.md#gettokenaccountsbyowner) +- [getTokenSupply](jsonrpc-api.md#gettokensupply) - [getTransactionCount](jsonrpc-api.md#gettransactioncount) - [getVersion](jsonrpc-api.md#getversion) - [getVoteAccounts](jsonrpc-api.md#getvoteaccounts) @@ -1016,6 +1020,116 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, " {"jsonrpc":"2.0","result":{"context":{"slot":1114},"value":{"circulating":16000,"nonCirculating":1000000,"nonCirculatingAccounts":["FEy8pTbP5fEoqMV1GdTz83byuA8EKByqYat1PKDgVAq5","9huDUZfxoJ7wGMTffUE7vh1xePqef7gyrLJu9NApncqA","3mi1GmwEE3zo2jmfDuzvjSX9ovRXsDUKHvsntpkhuLJ9","BYxEJTDerkaRWBem3XgnVcdhppktBXa2HbkHPKj2Ui4Z],total:1016000}},"id":1} ``` +### getTokenAccountBalance + +Returns the token balance of an SPL Token account. + +#### Parameters: + +- `` - Pubkey of Token account to query, as base-58 encoded string +- `` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) + +#### Results: + +- `RpcResponse` - RpcResponse JSON object with `value` field set to the balance + +#### Example: + +```bash +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getTokenAccountBalance", "params": ["7fUAJdStEuGbc3sM84cKRL6yYaaSstyLSU4ve5oovLS7"]}' http://localhost:8899 +// Result +{"jsonrpc":"2.0","result":{"context":{"slot":1114},"value":9864,"id":1} +``` + +### getTokenAccountsByDelegate + +Returns all SPL Token accounts by approved Delegate. + +#### Parameters: + +- `` - Pubkey of account delegate to query, as base-58 encoded string +- `` - Either: + * `mint: ` - Pubkey of the specific token Mint to limit accounts to, as base-58 encoded string; or + * `programId: ` - Pubkey of the Token program ID that owns the accounts, as base-58 encoded string +- `` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) + +#### Results: + +The result will be an RpcResponse JSON object with `value` equal to an array of JSON objects, which will contain: + +- `pubkey: ` - the account Pubkey as base-58 encoded string +- `account: ` - a JSON object, with the following sub fields: + - `lamports: `, number of lamports assigned to this account, as a u64 + - `owner: `, base-58 encoded Pubkey of the program this account has been assigned to + `data: `, Token state data associated with the account, in JSON format `{: }` + - `executable: `, boolean indicating if the account contains a program \(and is strictly read-only\) + - `rentEpoch: `, 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":"getTokenAccountsByDelegate", "params": ["4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T", {"programId": "TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"}]}' http://localhost:8899 +// Result +{"jsonrpc":"2.0","result":{"context":{"slot":1114},"value":[{"data":{"token":{"account":{"amount":1,"delegate":"4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T","delegatedAmount":1,"isInitialized":true,"isNative":false,"mint":"3wyAj7Rt1TWVPZVteFJPLa26JmLvdb1CAKEFZm3NY75E","owner":"CnPoSPKXu7wJqxe59Fs72tkBeALovhsCxYeFwPCQH9TD"}}},"executable":false,"lamports":1726080,"owner":"TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o","rentEpoch":4},"pubkey":"CnPoSPKXu7wJqxe59Fs72tkBeALovhsCxYeFwPCQH9TD"}],"id":1} +``` + +### getTokenAccountsByOwner + +Returns all SPL Token accounts by token owner. + +#### Parameters: + +- `` - Pubkey of account owner to query, as base-58 encoded string +- `` - Either: + * `mint: ` - Pubkey of the specific token Mint to limit accounts to, as base-58 encoded string; or + * `programId: ` - Pubkey of the Token program ID that owns the accounts, as base-58 encoded string +- `` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) + +#### Results: + +The result will be an RpcResponse JSON object with `value` equal to an array of JSON objects, which will contain: + +- `pubkey: ` - the account Pubkey as base-58 encoded string +- `account: ` - a JSON object, with the following sub fields: + - `lamports: `, number of lamports assigned to this account, as a u64 + - `owner: `, base-58 encoded Pubkey of the program this account has been assigned to + `data: `, Token state data associated with the account, in JSON format `{: }` + - `executable: `, boolean indicating if the account contains a program \(and is strictly read-only\) + - `rentEpoch: `, 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":"getTokenAccountsByOwner", "params": ["4Qkev8aNZcqFNSRhQzwyLMFSsi94jHqE8WNVTJzTP99F", {"mint":"3wyAj7Rt1TWVPZVteFJPLa26JmLvdb1CAKEFZm3NY75E"}]}' http://localhost:8899 +// Result +{"jsonrpc":"2.0","result":{"context":{"slot":1114},"value":[{"data":{"token":{"account":{"amount":1,"delegate":null,"delegatedAmount":1,"isInitialized":true,"isNative":false,"mint":"3wyAj7Rt1TWVPZVteFJPLa26JmLvdb1CAKEFZm3NY75E","owner":"4Qkev8aNZcqFNSRhQzwyLMFSsi94jHqE8WNVTJzTP99F"}}},"executable":false,"lamports":1726080,"owner":"TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o","rentEpoch":4},"pubkey":"CnPoSPKXu7wJqxe59Fs72tkBeALovhsCxYeFwPCQH9TD"}],"id":1} +``` + +### getTokenSupply + +Returns the total supply of an SPL Token type. + +#### Parameters: + +- `` - Pubkey of token Mint to query, as base-58 encoded string +- `` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) + +#### Results: + +- `RpcResponse` - RpcResponse JSON object with `value` field set to the total token supply + +#### Example: + +```bash +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getTokenSupply", "params": ["3wyAj7Rt1TWVPZVteFJPLa26JmLvdb1CAKEFZm3NY75E"]}' http://localhost:8899 +// Result +{"jsonrpc":"2.0","result":{"context":{"slot":1114},"value":100000,"id":1} +``` + ### getTransactionCount Returns the current Transaction count from the ledger diff --git a/transaction-status/Cargo.toml b/transaction-status/Cargo.toml index 638b5c812..3bc439727 100644 --- a/transaction-status/Cargo.toml +++ b/transaction-status/Cargo.toml @@ -16,7 +16,7 @@ lazy_static = "1.4.0" solana-sdk = { path = "../sdk", version = "1.3.0" } solana-stake-program = { path = "../programs/stake", version = "1.3.0" } solana-vote-program = { path = "../programs/vote", version = "1.3.0" } -spl-memo = { version = "1.0.4", features = ["skip-no-mangle"] } +spl-memo-v1-0 = { package = "spl-memo", version = "1.0.4", features = ["skip-no-mangle"] } serde = "1.0.112" serde_derive = "1.0.103" serde_json = "1.0.56" diff --git a/transaction-status/src/parse_instruction.rs b/transaction-status/src/parse_instruction.rs index 14bd14fd3..e6264b4f3 100644 --- a/transaction-status/src/parse_instruction.rs +++ b/transaction-status/src/parse_instruction.rs @@ -7,7 +7,8 @@ use std::{ }; lazy_static! { - static ref MEMO_PROGRAM_ID: Pubkey = Pubkey::from_str(&spl_memo::id().to_string()).unwrap(); + static ref MEMO_PROGRAM_ID: Pubkey = + Pubkey::from_str(&spl_memo_v1_0::id().to_string()).unwrap(); static ref PARSABLE_PROGRAM_IDS: HashMap = { let mut m = HashMap::new(); m.insert(*MEMO_PROGRAM_ID, ParsableProgram::SplMemo);