diff --git a/core/src/rpc_pubsub.rs b/core/src/rpc_pubsub.rs index 9f1c18c4be..119a915577 100644 --- a/core/src/rpc_pubsub.rs +++ b/core/src/rpc_pubsub.rs @@ -5,7 +5,10 @@ use jsonrpc_core::{Error, ErrorCode, Result}; use jsonrpc_derive::rpc; use jsonrpc_pubsub::{typed::Subscriber, Session, SubscriptionId}; use solana_account_decoder::UiAccount; -use solana_client::rpc_response::{Response as RpcResponse, RpcKeyedAccount, RpcSignatureResult}; +use solana_client::{ + rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, + rpc_response::{Response as RpcResponse, RpcKeyedAccount, RpcSignatureResult}, +}; #[cfg(test)] use solana_runtime::bank_forks::BankForks; use solana_sdk::{ @@ -38,7 +41,7 @@ pub trait RpcSolPubSub { meta: Self::Metadata, subscriber: Subscriber>, pubkey_str: String, - commitment: Option, + config: Option, ); // Unsubscribe from account notification subscription. @@ -62,7 +65,7 @@ pub trait RpcSolPubSub { meta: Self::Metadata, subscriber: Subscriber>, pubkey_str: String, - commitment: Option, + config: Option, ); // Unsubscribe from account notification subscription. @@ -173,7 +176,7 @@ impl RpcSolPubSub for RpcSolPubSubImpl { _meta: Self::Metadata, subscriber: Subscriber>, pubkey_str: String, - commitment: Option, + config: Option, ) { match param::(&pubkey_str, "pubkey") { Ok(pubkey) => { @@ -181,7 +184,7 @@ impl RpcSolPubSub for RpcSolPubSubImpl { let sub_id = SubscriptionId::Number(id as u64); info!("account_subscribe: account={:?} id={:?}", pubkey, sub_id); self.subscriptions - .add_account_subscription(pubkey, commitment, sub_id, subscriber) + .add_account_subscription(pubkey, config, sub_id, subscriber) } Err(e) => subscriber.reject(e).unwrap(), } @@ -209,7 +212,7 @@ impl RpcSolPubSub for RpcSolPubSubImpl { _meta: Self::Metadata, subscriber: Subscriber>, pubkey_str: String, - commitment: Option, + config: Option, ) { match param::(&pubkey_str, "pubkey") { Ok(pubkey) => { @@ -217,7 +220,7 @@ impl RpcSolPubSub for RpcSolPubSubImpl { let sub_id = SubscriptionId::Number(id as u64); info!("program_subscribe: account={:?} id={:?}", pubkey, sub_id); self.subscriptions - .add_program_subscription(pubkey, commitment, sub_id, subscriber) + .add_program_subscription(pubkey, config, sub_id, subscriber) } Err(e) => subscriber.reject(e).unwrap(), } @@ -355,6 +358,7 @@ mod tests { use jsonrpc_core::{futures::sync::mpsc, Response}; use jsonrpc_pubsub::{PubSubHandler, Session}; use serial_test_derive::serial; + use solana_account_decoder::{parse_account_data::parse_account_data, UiAccountEncoding}; use solana_budget_program::{self, budget_instruction}; use solana_runtime::{ bank::Bank, @@ -370,7 +374,7 @@ mod tests { message::Message, pubkey::Pubkey, signature::{Keypair, Signer}, - system_program, system_transaction, + system_instruction, system_program, system_transaction, transaction::{self, Transaction}, }; use solana_vote_program::vote_transaction; @@ -537,7 +541,10 @@ mod tests { session, subscriber, contract_state.pubkey().to_string(), - Some(CommitmentConfig::recent()), + Some(RpcAccountInfoConfig { + commitment: Some(CommitmentConfig::recent()), + encoding: None, + }), ); let tx = system_transaction::transfer(&alice, &contract_funds.pubkey(), 51, blockhash); @@ -610,6 +617,88 @@ mod tests { ); } + #[test] + #[serial] + fn test_account_subscribe_with_encoding() { + let GenesisConfigInfo { + genesis_config, + mint_keypair: alice, + .. + } = create_genesis_config(10_000); + + let nonce_account = Keypair::new(); + let bank = Bank::new(&genesis_config); + let blockhash = bank.last_blockhash(); + let bank_forks = Arc::new(RwLock::new(BankForks::new(bank))); + let bank0 = bank_forks.read().unwrap().get(0).unwrap().clone(); + let bank1 = Bank::new_from_parent(&bank0, &Pubkey::default(), 1); + bank_forks.write().unwrap().insert(bank1); + + let rpc = RpcSolPubSubImpl { + subscriptions: Arc::new(RpcSubscriptions::new( + &Arc::new(AtomicBool::new(false)), + bank_forks.clone(), + Arc::new(RwLock::new(BlockCommitmentCache::new_for_tests_with_slots( + 1, 1, + ))), + )), + uid: Arc::new(atomic::AtomicUsize::default()), + }; + let session = create_session(); + let (subscriber, _id_receiver, receiver) = Subscriber::new_test("accountNotification"); + rpc.account_subscribe( + session, + subscriber, + nonce_account.pubkey().to_string(), + Some(RpcAccountInfoConfig { + commitment: Some(CommitmentConfig::recent()), + encoding: Some(UiAccountEncoding::JsonParsed), + }), + ); + + let ixs = system_instruction::create_nonce_account( + &alice.pubkey(), + &nonce_account.pubkey(), + &alice.pubkey(), + 100, + ); + let message = Message::new(&ixs, Some(&alice.pubkey())); + let tx = Transaction::new(&[&alice, &nonce_account], message, blockhash); + process_transaction_and_notify(&bank_forks, &tx, &rpc.subscriptions, 1).unwrap(); + sleep(Duration::from_millis(200)); + + // Test signature confirmation notification #1 + let expected_data = bank_forks + .read() + .unwrap() + .get(1) + .unwrap() + .get_account(&nonce_account.pubkey()) + .unwrap() + .data; + let expected_data = parse_account_data(&system_program::id(), &expected_data).unwrap(); + let expected = json!({ + "jsonrpc": "2.0", + "method": "accountNotification", + "params": { + "result": { + "context": { "slot": 1 }, + "value": { + "owner": system_program::id().to_string(), + "lamports": 100, + "data": expected_data, + "executable": false, + "rentEpoch": 1, + }, + }, + "subscription": 0, + } + }); + + let (response, _) = robust_poll_or_panic(receiver); + assert_eq!(serde_json::to_string(&expected).unwrap(), response); + } + #[test] #[serial] fn test_account_unsubscribe() { @@ -675,7 +764,10 @@ mod tests { session, subscriber, bob.pubkey().to_string(), - Some(CommitmentConfig::root()), + Some(RpcAccountInfoConfig { + commitment: Some(CommitmentConfig::root()), + encoding: None, + }), ); let tx = system_transaction::transfer(&alice, &bob.pubkey(), 100, blockhash); @@ -721,7 +813,10 @@ mod tests { session, subscriber, bob.pubkey().to_string(), - Some(CommitmentConfig::root()), + Some(RpcAccountInfoConfig { + commitment: Some(CommitmentConfig::root()), + encoding: None, + }), ); let tx = system_transaction::transfer(&alice, &bob.pubkey(), 100, blockhash); diff --git a/core/src/rpc_subscriptions.rs b/core/src/rpc_subscriptions.rs index 11b5e63205..b4b1c9dbb2 100644 --- a/core/src/rpc_subscriptions.rs +++ b/core/src/rpc_subscriptions.rs @@ -8,8 +8,10 @@ use jsonrpc_pubsub::{ }; use serde::Serialize; use solana_account_decoder::{UiAccount, UiAccountEncoding}; -use solana_client::rpc_response::{ - Response, RpcKeyedAccount, RpcResponseContext, RpcSignatureResult, +use solana_client::{ + rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, + rpc_filter::RpcFilterType, + rpc_response::{Response, RpcKeyedAccount, RpcResponseContext, RpcSignatureResult}, }; use solana_runtime::{ bank::Bank, @@ -79,29 +81,44 @@ impl std::fmt::Debug for NotificationEntry { } } -struct SubscriptionData { +struct SubscriptionData { sink: Sink, commitment: CommitmentConfig, last_notified_slot: RwLock, + config: Option, } -type RpcAccountSubscriptions = - RwLock>>>>; -type RpcProgramSubscriptions = - RwLock>>>>; +#[derive(Default, Clone)] +struct ProgramConfig { + filters: Vec, + encoding: Option, +} +type RpcAccountSubscriptions = RwLock< + HashMap< + Pubkey, + HashMap, UiAccountEncoding>>, + >, +>; +type RpcProgramSubscriptions = RwLock< + HashMap< + Pubkey, + HashMap, ProgramConfig>>, + >, +>; type RpcSignatureSubscriptions = RwLock< - HashMap>>>, + HashMap, ()>>>, >; type RpcSlotSubscriptions = RwLock>>; type RpcVoteSubscriptions = RwLock>>; type RpcRootSubscriptions = RwLock>>; -fn add_subscription( - subscriptions: &mut HashMap>>, +fn add_subscription( + subscriptions: &mut HashMap>>, hashmap_key: K, commitment: Option, sub_id: SubscriptionId, subscriber: Subscriber, last_notified_slot: Slot, + config: Option, ) where K: Eq + Hash, S: Clone, @@ -112,6 +129,7 @@ fn add_subscription( sink, commitment, last_notified_slot: RwLock::new(last_notified_slot), + config, }; if let Some(current_hashmap) = subscriptions.get_mut(&hashmap_key) { current_hashmap.insert(sub_id, subscription_data); @@ -122,8 +140,8 @@ fn add_subscription( subscriptions.insert(hashmap_key, hashmap); } -fn remove_subscription( - subscriptions: &mut HashMap>>, +fn remove_subscription( + subscriptions: &mut HashMap>>, sub_id: &SubscriptionId, ) -> bool where @@ -145,8 +163,8 @@ where } #[allow(clippy::type_complexity)] -fn check_commitment_and_notify( - subscriptions: &HashMap>>>, +fn check_commitment_and_notify( + subscriptions: &HashMap, T>>>, hashmap_key: &K, bank_forks: &Arc>, commitment_slots: &CommitmentSlots, @@ -158,8 +176,9 @@ where K: Eq + Hash + Clone + Copy, S: Clone + Serialize, B: Fn(&Bank, &K) -> X, - F: Fn(X, Slot) -> (Box>, Slot), + F: Fn(X, Slot, Option) -> (Box>, Slot), X: Clone + Serialize + Default, + T: Clone, { let mut notified_set: HashSet = HashSet::new(); if let Some(hashmap) = subscriptions.get(hashmap_key) { @@ -169,6 +188,7 @@ where sink, commitment, last_notified_slot, + config, }, ) in hashmap.iter() { @@ -188,7 +208,8 @@ where .unwrap_or_default() }; let mut w_last_notified_slot = last_notified_slot.write().unwrap(); - let (filter_results, result_slot) = filter_results(results, *w_last_notified_slot); + let (filter_results, result_slot) = + filter_results(results, *w_last_notified_slot, config.as_ref().cloned()); for result in filter_results { notifier.notify( Response { @@ -220,16 +241,15 @@ impl RpcNotifier { fn filter_account_result( result: Option<(Account, Slot)>, last_notified_slot: Slot, + encoding: Option, ) -> (Box>, Slot) { if let Some((account, fork)) = result { // If fork < last_notified_slot this means that we last notified for a fork // and should notify that the account state has been reverted. if fork != last_notified_slot { + let encoding = encoding.unwrap_or(UiAccountEncoding::Binary); return ( - Box::new(iter::once(UiAccount::encode( - account, - UiAccountEncoding::Binary, - ))), + Box::new(iter::once(UiAccount::encode(account, encoding))), fork, ); } @@ -240,6 +260,7 @@ fn filter_account_result( fn filter_signature_result( result: Option>, last_notified_slot: Slot, + _config: Option<()>, ) -> (Box>, Slot) { ( Box::new( @@ -254,14 +275,24 @@ fn filter_signature_result( fn filter_program_results( accounts: Vec<(Pubkey, Account)>, last_notified_slot: Slot, + config: Option, ) -> (Box>, Slot) { + let config = config.unwrap_or_default(); + let encoding = config.encoding.unwrap_or(UiAccountEncoding::Binary); + let filters = config.filters; ( Box::new( accounts .into_iter() - .map(|(pubkey, account)| RpcKeyedAccount { + .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), + }) + }) + .map(move |(pubkey, account)| RpcKeyedAccount { pubkey: pubkey.to_string(), - account: UiAccount::encode(account, UiAccountEncoding::Binary), + account: UiAccount::encode(account, encoding.clone()), }), ), last_notified_slot, @@ -448,11 +479,13 @@ impl RpcSubscriptions { pub fn add_account_subscription( &self, pubkey: Pubkey, - commitment: Option, + config: Option, sub_id: SubscriptionId, subscriber: Subscriber>, ) { - let commitment_level = commitment + let config = config.unwrap_or_default(); + let commitment_level = config + .commitment .unwrap_or_else(CommitmentConfig::single) .commitment; let slot = match commitment_level { @@ -498,10 +531,11 @@ impl RpcSubscriptions { add_subscription( &mut subscriptions, pubkey, - commitment, + config.commitment, sub_id, subscriber, last_notified_slot, + config.encoding, ); } @@ -522,11 +556,14 @@ impl RpcSubscriptions { pub fn add_program_subscription( &self, program_id: Pubkey, - commitment: Option, + config: Option, sub_id: SubscriptionId, subscriber: Subscriber>, ) { - let commitment_level = commitment + let config = config.unwrap_or_default(); + let commitment_level = config + .account_config + .commitment .unwrap_or_else(CommitmentConfig::recent) .commitment; let mut subscriptions = if commitment_level == CommitmentLevel::SingleGossip { @@ -540,10 +577,14 @@ impl RpcSubscriptions { add_subscription( &mut subscriptions, program_id, - commitment, + config.account_config.commitment, sub_id, subscriber, 0, // last_notified_slot is not utilized for program subscriptions + Some(ProgramConfig { + filters: config.filters.unwrap_or_default(), + encoding: config.account_config.encoding, + }), ); } @@ -586,6 +627,7 @@ impl RpcSubscriptions { sub_id, subscriber, 0, // last_notified_slot is not utilized for signature subscriptions + None, ); } @@ -964,7 +1006,10 @@ pub(crate) mod tests { ); subscriptions.add_account_subscription( alice.pubkey(), - Some(CommitmentConfig::recent()), + Some(RpcAccountInfoConfig { + commitment: Some(CommitmentConfig::recent()), + encoding: None, + }), sub_id.clone(), subscriber, ); @@ -1368,7 +1413,7 @@ pub(crate) mod tests { #[test] #[serial] fn test_add_and_remove_subscription() { - let mut subscriptions: HashMap>> = + let mut subscriptions: HashMap>> = HashMap::new(); let num_keys = 5; @@ -1376,7 +1421,7 @@ pub(crate) mod tests { let (subscriber, _id_receiver, _transport_receiver) = Subscriber::new_test("notification"); let sub_id = SubscriptionId::Number(key); - add_subscription(&mut subscriptions, key, None, sub_id, subscriber, 0); + add_subscription(&mut subscriptions, key, None, sub_id, subscriber, 0, None); } // Add another subscription to the "0" key @@ -1389,6 +1434,7 @@ pub(crate) mod tests { extra_sub_id.clone(), subscriber, 0, + None, ); assert_eq!(subscriptions.len(), num_keys as usize); @@ -1444,7 +1490,10 @@ pub(crate) mod tests { let sub_id0 = SubscriptionId::Number(0 as u64); subscriptions.add_account_subscription( alice.pubkey(), - Some(CommitmentConfig::single_gossip()), + Some(RpcAccountInfoConfig { + commitment: Some(CommitmentConfig::single_gossip()), + encoding: None, + }), sub_id0.clone(), subscriber0, ); @@ -1509,7 +1558,10 @@ pub(crate) mod tests { let sub_id1 = SubscriptionId::Number(1 as u64); subscriptions.add_account_subscription( alice.pubkey(), - Some(CommitmentConfig::single_gossip()), + Some(RpcAccountInfoConfig { + commitment: Some(CommitmentConfig::single_gossip()), + encoding: None, + }), sub_id1.clone(), subscriber1, ); diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index 72c775044d..5ca73c1957 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -1256,7 +1256,10 @@ Subscribe to an account to receive notifications when the lamports or data for a #### Parameters: - `` - account Pubkey, as base-58 encoded string -- `` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) +- `` - (optional) Configuration object containing the following optional fields: + - `` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) + - (optional) `encoding: ` - encoding for Account data, either "binary" or jsonParsed". If parameter not provided, the default encoding is binary. + 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 binary encoding, detectable when the `data` field is type ``. #### Results: @@ -1270,13 +1273,16 @@ Subscribe to an account to receive notifications when the lamports or data for a {"jsonrpc":"2.0", "id":1, "method":"accountSubscribe", "params":["CM78CPUeXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNH12", {"commitment": "single"}]} +{"jsonrpc":"2.0", "id":1, "method":"accountSubscribe", "params":["CM78CPUeXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNH12", {"encoding":"jsonParsed"}]} + // Result -{"jsonrpc": "2.0","result": 0,"id": 1} +{"jsonrpc": "2.0","result": 23784,"id": 1} ``` #### Notification Format: ```bash +// Binary encoding { "jsonrpc": "2.0", "method": "accountNotification", @@ -1286,10 +1292,41 @@ Subscribe to an account to receive notifications when the lamports or data for a "slot": 5199307 }, "value": { - "data": "9qRxMDwy1ntDhBBoiy4Na9uDLbRTSzUS989mpwz", + "data": "11116bv5nS2h3y12kD1yUKeMZvGcKLSjQgX6BeV7u1FrjeJcKfsHPXHRDEHrBesJhZyqnnq9qJeUuF7WHxiuLuL5twc38w2TXNLxnDbjmuR", "executable": false, "lamports": 33594, - "owner": "H9oaJujXETwkmjyweuqKPFtk2no4SumoU9A3hi3dC8U6", + "owner": "11111111111111111111111111111111", + "rentEpoch": 635 + } + }, + "subscription": 23784 + } +} + +// Parsed-JSON encoding +{ + "jsonrpc": "2.0", + "method": "accountNotification", + "params": { + "result": { + "context": { + "slot": 5199307 + }, + "value": { + "data": { + "nonce": { + "initialized": { + "authority": "Bbqg1M4YVVfbhEzwA9SpC9FhsaG83YMTYoR4a8oTDLX", + "blockhash": "LUaQTmM7WbMRiATdMMHaRGakPtCkc2GHtH57STKXs6k", + "feeCalculator": { + "lamportsPerSignature": 5000 + } + } + } + }, + "executable": false, + "lamports": 33594, + "owner": "11111111111111111111111111111111", "rentEpoch": 635 } }, @@ -1327,7 +1364,11 @@ Subscribe to a program to receive notifications when the lamports or data for a #### Parameters: - `` - program_id Pubkey, as base-58 encoded string -- `` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) +- `` - (optional) Configuration object containing the following optional fields: + - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) + - (optional) `encoding: ` - encoding for Account data, either "binary" or jsonParsed". If parameter not provided, the default encoding is binary. + 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 binary encoding, detectable when the `data` field is type ``. + - (optional) `filters: ` - filter results using various [filter objects](jsonrpc-api.md#filters); account must meet all filter criteria to be included in results #### Results: @@ -1337,17 +1378,22 @@ Subscribe to a program to receive notifications when the lamports or data for a ```bash // Request -{"jsonrpc":"2.0", "id":1, "method":"programSubscribe", "params":["7BwE8yitxiWkD8jVPFvPmV7rs2Znzi4NHzJGLu2dzpUq"]} +{"jsonrpc":"2.0", "id":1, "method":"programSubscribe", "params":["11111111111111111111111111111111"]} -{"jsonrpc":"2.0", "id":1, "method":"programSubscribe", "params":["7BwE8yitxiWkD8jVPFvPmV7rs2Znzi4NHzJGLu2dzpUq", {"commitment": "single"}]} +{"jsonrpc":"2.0", "id":1, "method":"programSubscribe", "params":["11111111111111111111111111111111", {"commitment": "single"}]} + +{"jsonrpc":"2.0", "id":1, "method":"programSubscribe", "params":["11111111111111111111111111111111", {"encoding":"jsonParsed"}]} + +{"jsonrpc":"2.0", "id":1, "method":"programSubscribe", "params":["11111111111111111111111111111111", {"filters":[{"dataSize":80}]}]} // Result -{"jsonrpc": "2.0","result": 0,"id": 1} +{"jsonrpc": "2.0","result": 24040,"id": 1} ``` #### Notification Format: ```bash +// Binary encoding { "jsonrpc": "2.0", "method": "programNotification", @@ -1359,10 +1405,44 @@ Subscribe to a program to receive notifications when the lamports or data for a "value": { "pubkey": "H4vnBqifaSACnKa7acsxstsY1iV1bvJNxsCY7enrd1hq" "account": { - "data": "9qRxMDwy1ntDhBBoiy4Na9uDLbRTSzUS989m", + "data": "11116bv5nS2h3y12kD1yUKeMZvGcKLSjQgX6BeV7u1FrjeJcKfsHPXHRDEHrBesJhZyqnnq9qJeUuF7WHxiuLuL5twc38w2TXNLxnDbjmuR", "executable": false, "lamports": 33594, - "owner": "7BwE8yitxiWkD8jVPFvPmV7rs2Znzi4NHzJGLu2dzpUq", + "owner": "11111111111111111111111111111111", + "rentEpoch": 636 + }, + } + }, + "subscription": 24040 + } +} + +// Parsed-JSON encoding +{ + "jsonrpc": "2.0", + "method": "programNotification", + "params": { + "result": { + "context": { + "slot": 5208469 + }, + "value": { + "pubkey": "H4vnBqifaSACnKa7acsxstsY1iV1bvJNxsCY7enrd1hq" + "account": { + "data": { + "nonce": { + "initialized": { + "authority": "Bbqg1M4YVVfbhEzwA9SpC9FhsaG83YMTYoR4a8oTDLX", + "blockhash": "LUaQTmM7WbMRiATdMMHaRGakPtCkc2GHtH57STKXs6k", + "feeCalculator": { + "lamportsPerSignature": 5000 + } + } + } + }, + "executable": false, + "lamports": 33594, + "owner": "11111111111111111111111111111111", "rentEpoch": 636 }, }