From 632035ffc1d0fcb9282d89d6f361396fd2d1175d Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Tue, 9 Jul 2024 21:01:12 -0600 Subject: [PATCH 01/10] Add test to demonstrate serialization and acceptable json inputs --- Cargo.lock | 1 + rpc-client-api/Cargo.toml | 1 + rpc-client-api/src/filter.rs | 186 ++++++++++++++++++++++++++++++++++- 3 files changed, 187 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index db6a3e4d40e994..3418b947393076 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7090,6 +7090,7 @@ dependencies = [ "anyhow", "base64 0.22.1", "bs58", + "const_format", "jsonrpc-core", "reqwest", "reqwest-middleware", diff --git a/rpc-client-api/Cargo.toml b/rpc-client-api/Cargo.toml index c8d1eaad8b7959..22a883244c709e 100644 --- a/rpc-client-api/Cargo.toml +++ b/rpc-client-api/Cargo.toml @@ -28,6 +28,7 @@ solana-version = { workspace = true } thiserror = { workspace = true } [dev-dependencies] +const_format = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/rpc-client-api/src/filter.rs b/rpc-client-api/src/filter.rs index bc94da2938b8e8..a64fa25b0e3068 100644 --- a/rpc-client-api/src/filter.rs +++ b/rpc-client-api/src/filter.rs @@ -299,7 +299,11 @@ impl From for Memcmp { #[cfg(test)] mod tests { - use super::*; + use { + super::*, + const_format::formatcp, + serde_json::{json, Value}, + }; #[test] fn test_worst_case_encoded_tx_goldens() { @@ -402,4 +406,184 @@ mod tests { Err(RpcFilterError::DataTooLarge) ); } + + const BASE58_STR: &str = "Bpf4ERpEvSFmCSTNh1PzTWTkALrKXvMXEdthxHuwCQcf"; + const BASE64_STR: &str = "oMoycDvJzrjQpCfukbO4VW/FLGLfnbqBEc9KUEVgj2g="; + const BYTES: [u8; 4] = [0, 1, 2, 3]; + const OFFSET: usize = 42; + const DEFAULT_ENCODING_FILTER: &str = + formatcp!(r#"{{"bytes":"{BASE58_STR}","offset":{OFFSET}}}"#); + const BINARY_FILTER: &str = + formatcp!(r#"{{"bytes":"{BASE58_STR}","offset":{OFFSET},"encoding":"binary"}}"#); + const BASE58_FILTER: &str = + formatcp!(r#"{{"bytes":"{BASE58_STR}","offset":{OFFSET},"encoding":"base58"}}"#); + const BASE64_FILTER: &str = + formatcp!(r#"{{"bytes":"{BASE64_STR}","offset":{OFFSET},"encoding":"base64"}}"#); + const BYTES_FILTER: &str = + formatcp!(r#"{{"bytes":[0, 1, 2, 3],"offset":{OFFSET},"encoding":null}}"#); + const BYTES_FILTER_WITH_ENCODING: &str = + formatcp!(r#"{{"bytes":[0, 1, 2, 3],"offset":{OFFSET},"encoding":"bytes"}}"#); + + #[test] + fn test_filter_deserialize() { + // Base58 is the default encoding + let default: Memcmp = serde_json::from_str(DEFAULT_ENCODING_FILTER).unwrap(); + assert_eq!( + default, + Memcmp { + offset: OFFSET, + bytes: MemcmpEncodedBytes::Base58(BASE58_STR.to_string()), + encoding: None, + } + ); + + // Binary input is deserialized as Base58 + let binary: Memcmp = serde_json::from_str(BINARY_FILTER).unwrap(); + assert_eq!( + binary, + Memcmp { + offset: OFFSET, + bytes: MemcmpEncodedBytes::Base58(BASE58_STR.to_string()), + encoding: None, + } + ); + + // Base58 input + let base58_filter: Memcmp = serde_json::from_str(BASE58_FILTER).unwrap(); + assert_eq!( + base58_filter, + Memcmp { + offset: OFFSET, + bytes: MemcmpEncodedBytes::Base58(BASE58_STR.to_string()), + encoding: None, + } + ); + + // Base64 input + let base64_filter: Memcmp = serde_json::from_str(BASE64_FILTER).unwrap(); + assert_eq!( + base64_filter, + Memcmp { + offset: OFFSET, + bytes: MemcmpEncodedBytes::Base64(BASE64_STR.to_string()), + encoding: None, + } + ); + + // Raw bytes input + let bytes_filter: Memcmp = serde_json::from_str(BYTES_FILTER).unwrap(); + assert_eq!( + bytes_filter, + Memcmp { + offset: OFFSET, + bytes: MemcmpEncodedBytes::Bytes(BYTES.to_vec()), + encoding: None, + } + ); + + let bytes_filter: Memcmp = serde_json::from_str(BYTES_FILTER_WITH_ENCODING).unwrap(); + assert_eq!( + bytes_filter, + Memcmp { + offset: OFFSET, + bytes: MemcmpEncodedBytes::Bytes(BYTES.to_vec()), + encoding: None, + } + ); + } + + #[test] + fn test_filter_serialize() { + // Binary variants + let binary_default = Memcmp { + offset: OFFSET, + bytes: MemcmpEncodedBytes::Binary(BASE58_STR.to_string()), + encoding: None, + }; + let serialized_json = json!(binary_default); + assert_eq!( + serialized_json, + serde_json::from_str::(BINARY_FILTER).unwrap() + ); + + let binary_default = Memcmp { + offset: OFFSET, + bytes: MemcmpEncodedBytes::Binary(BASE58_STR.to_string()), + encoding: Some(MemcmpEncoding::Binary), + }; + let serialized_json = json!(binary_default); + assert_eq!( + serialized_json, + serde_json::from_str::(BINARY_FILTER).unwrap() + ); + + let binary_base58 = Memcmp { + offset: OFFSET, + bytes: MemcmpEncodedBytes::Base58(BASE58_STR.to_string()), + encoding: Some(MemcmpEncoding::Binary), + }; + let serialized_json = json!(binary_base58); + assert_eq!( + serialized_json, + serde_json::from_str::(BASE58_FILTER).unwrap() + ); + + let binary_base64 = Memcmp { + offset: OFFSET, + bytes: MemcmpEncodedBytes::Base64(BASE64_STR.to_string()), + encoding: Some(MemcmpEncoding::Binary), + }; + let serialized_json = json!(binary_base64); + assert_eq!( + serialized_json, + serde_json::from_str::(BASE64_FILTER).unwrap() + ); + + let binary_bytes = Memcmp { + offset: OFFSET, + bytes: MemcmpEncodedBytes::Bytes(BYTES.to_vec()), + encoding: Some(MemcmpEncoding::Binary), + }; + let serialized_json = json!(binary_bytes); + assert_eq!( + serialized_json, + serde_json::from_str::(BYTES_FILTER).unwrap() + ); + + // Base58 + let base58 = Memcmp { + offset: OFFSET, + bytes: MemcmpEncodedBytes::Base58(BASE58_STR.to_string()), + encoding: None, + }; + let serialized_json = json!(base58); + assert_eq!( + serialized_json, + serde_json::from_str::(BASE58_FILTER).unwrap() + ); + + // Base64 + let base64 = Memcmp { + offset: OFFSET, + bytes: MemcmpEncodedBytes::Base64(BASE64_STR.to_string()), + encoding: None, + }; + let serialized_json = json!(base64); + assert_eq!( + serialized_json, + serde_json::from_str::(BASE64_FILTER).unwrap() + ); + + // Bytes + let bytes = Memcmp { + offset: OFFSET, + bytes: MemcmpEncodedBytes::Bytes(BYTES.to_vec()), + encoding: None, + }; + let serialized_json = json!(bytes); + assert_eq!( + serialized_json, + serde_json::from_str::(BYTES_FILTER).unwrap() + ); + } } From 57bd9106b9dca8b50eabde37997694b9e066cb8d Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Mon, 8 Jul 2024 16:44:32 -0600 Subject: [PATCH 02/10] Remove custom serialization --- rpc-client-api/src/filter.rs | 79 ++---------------------------------- 1 file changed, 3 insertions(+), 76 deletions(-) diff --git a/rpc-client-api/src/filter.rs b/rpc-client-api/src/filter.rs index a64fa25b0e3068..8ef324ee49f809 100644 --- a/rpc-client-api/src/filter.rs +++ b/rpc-client-api/src/filter.rs @@ -120,7 +120,7 @@ pub enum MemcmpEncoding { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", untagged)] +#[serde(rename_all = "camelCase", tag = "encoding", content = "bytes")] pub enum MemcmpEncodedBytes { #[deprecated( since = "1.8.1", @@ -133,7 +133,6 @@ pub enum MemcmpEncodedBytes { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(into = "RpcMemcmp", from = "RpcMemcmp")] pub struct Memcmp { /// Data offset to begin match #[deprecated( @@ -146,6 +145,7 @@ pub struct Memcmp { since = "1.15.0", note = "Field will be made private in future. Please use a constructor method instead." )] + #[serde(flatten)] pub bytes: MemcmpEncodedBytes, /// Optional encoding specification #[deprecated( @@ -153,6 +153,7 @@ pub struct Memcmp { note = "Field has no server-side effect. Specify encoding with `MemcmpEncodedBytes` variant instead. \ Field will be made private in future. Please use a constructor method instead." )] + #[serde(skip)] pub encoding: Option, } @@ -223,80 +224,6 @@ impl Memcmp { } } -// Internal struct to hold Memcmp filter data as either encoded String or raw Bytes -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(untagged)] -enum DataType { - Encoded(String), - Raw(Vec), -} - -// Internal struct used to specify explicit Base58 and Base64 encoding -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -enum RpcMemcmpEncoding { - Base58, - Base64, - // This variant exists only to preserve backward compatibility with generic `Memcmp` serde - #[serde(other)] - Binary, -} - -// Internal struct to enable Memcmp filters with explicit Base58 and Base64 encoding. The From -// implementations emulate `#[serde(tag = "encoding", content = "bytes")]` for -// `MemcmpEncodedBytes`. On the next major version, all these internal elements should be removed -// and replaced with adjacent tagging of `MemcmpEncodedBytes`. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct RpcMemcmp { - offset: usize, - bytes: DataType, - encoding: Option, -} - -impl From for RpcMemcmp { - fn from(memcmp: Memcmp) -> RpcMemcmp { - let (bytes, encoding) = match memcmp.bytes { - MemcmpEncodedBytes::Binary(string) => { - (DataType::Encoded(string), Some(RpcMemcmpEncoding::Binary)) - } - MemcmpEncodedBytes::Base58(string) => { - (DataType::Encoded(string), Some(RpcMemcmpEncoding::Base58)) - } - MemcmpEncodedBytes::Base64(string) => { - (DataType::Encoded(string), Some(RpcMemcmpEncoding::Base64)) - } - MemcmpEncodedBytes::Bytes(vector) => (DataType::Raw(vector), None), - }; - RpcMemcmp { - offset: memcmp.offset, - bytes, - encoding, - } - } -} - -impl From for Memcmp { - fn from(memcmp: RpcMemcmp) -> Memcmp { - let encoding = memcmp.encoding.unwrap_or(RpcMemcmpEncoding::Binary); - let bytes = match (encoding, memcmp.bytes) { - (RpcMemcmpEncoding::Binary, DataType::Encoded(string)) - | (RpcMemcmpEncoding::Base58, DataType::Encoded(string)) => { - MemcmpEncodedBytes::Base58(string) - } - (RpcMemcmpEncoding::Binary, DataType::Raw(vector)) => MemcmpEncodedBytes::Bytes(vector), - (RpcMemcmpEncoding::Base64, DataType::Encoded(string)) => { - MemcmpEncodedBytes::Base64(string) - } - _ => unreachable!(), - }; - Memcmp { - offset: memcmp.offset, - bytes, - encoding: None, - } - } -} - #[cfg(test)] mod tests { use { From e4e6e9a66da719c14f37929d84694077ebcfe134 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Mon, 8 Jul 2024 16:53:09 -0600 Subject: [PATCH 03/10] Unpub deprecated fields --- rpc-client-api/src/filter.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/rpc-client-api/src/filter.rs b/rpc-client-api/src/filter.rs index 8ef324ee49f809..123443da0f49a8 100644 --- a/rpc-client-api/src/filter.rs +++ b/rpc-client-api/src/filter.rs @@ -135,18 +135,10 @@ pub enum MemcmpEncodedBytes { #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Memcmp { /// Data offset to begin match - #[deprecated( - since = "1.15.0", - note = "Field will be made private in future. Please use a constructor method instead." - )] - pub offset: usize, + offset: usize, /// Bytes, encoded with specified encoding, or default Binary - #[deprecated( - since = "1.15.0", - note = "Field will be made private in future. Please use a constructor method instead." - )] #[serde(flatten)] - pub bytes: MemcmpEncodedBytes, + bytes: MemcmpEncodedBytes, /// Optional encoding specification #[deprecated( since = "1.11.2", From 2b9d7dd412a72b293eb711d9ccf0c2000e3be351 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Mon, 8 Jul 2024 16:56:11 -0600 Subject: [PATCH 04/10] Remove deprecated, unused field --- rpc-client-api/src/filter.rs | 109 ++++++++++++++--------------------- 1 file changed, 42 insertions(+), 67 deletions(-) diff --git a/rpc-client-api/src/filter.rs b/rpc-client-api/src/filter.rs index 123443da0f49a8..ac5668d61678a8 100644 --- a/rpc-client-api/src/filter.rs +++ b/rpc-client-api/src/filter.rs @@ -23,54 +23,49 @@ impl RpcFilterType { match self { RpcFilterType::DataSize(_) => Ok(()), RpcFilterType::Memcmp(compare) => { - let encoding = compare.encoding.as_ref().unwrap_or(&MemcmpEncoding::Binary); - match encoding { - MemcmpEncoding::Binary => { - use MemcmpEncodedBytes::*; - match &compare.bytes { - // DEPRECATED - Binary(bytes) => { - if bytes.len() > MAX_DATA_BASE58_SIZE { - return Err(RpcFilterError::Base58DataTooLarge); - } - let bytes = bs58::decode(&bytes) - .into_vec() - .map_err(RpcFilterError::DecodeError)?; - if bytes.len() > MAX_DATA_SIZE { - Err(RpcFilterError::Base58DataTooLarge) - } else { - Ok(()) - } - } - Base58(bytes) => { - if bytes.len() > MAX_DATA_BASE58_SIZE { - return Err(RpcFilterError::DataTooLarge); - } - let bytes = bs58::decode(&bytes).into_vec()?; - if bytes.len() > MAX_DATA_SIZE { - Err(RpcFilterError::DataTooLarge) - } else { - Ok(()) - } - } - Base64(bytes) => { - if bytes.len() > MAX_DATA_BASE64_SIZE { - return Err(RpcFilterError::DataTooLarge); - } - let bytes = base64::decode(bytes)?; - if bytes.len() > MAX_DATA_SIZE { - Err(RpcFilterError::DataTooLarge) - } else { - Ok(()) - } - } - Bytes(bytes) => { - if bytes.len() > MAX_DATA_SIZE { - return Err(RpcFilterError::DataTooLarge); - } - Ok(()) - } + use MemcmpEncodedBytes::*; + match &compare.bytes { + // DEPRECATED + Binary(bytes) => { + if bytes.len() > MAX_DATA_BASE58_SIZE { + return Err(RpcFilterError::Base58DataTooLarge); } + let bytes = bs58::decode(&bytes) + .into_vec() + .map_err(RpcFilterError::DecodeError)?; + if bytes.len() > MAX_DATA_SIZE { + Err(RpcFilterError::Base58DataTooLarge) + } else { + Ok(()) + } + } + Base58(bytes) => { + if bytes.len() > MAX_DATA_BASE58_SIZE { + return Err(RpcFilterError::DataTooLarge); + } + let bytes = bs58::decode(&bytes).into_vec()?; + if bytes.len() > MAX_DATA_SIZE { + Err(RpcFilterError::DataTooLarge) + } else { + Ok(()) + } + } + Base64(bytes) => { + if bytes.len() > MAX_DATA_BASE64_SIZE { + return Err(RpcFilterError::DataTooLarge); + } + let bytes = base64::decode(bytes)?; + if bytes.len() > MAX_DATA_SIZE { + Err(RpcFilterError::DataTooLarge) + } else { + Ok(()) + } + } + Bytes(bytes) => { + if bytes.len() > MAX_DATA_SIZE { + return Err(RpcFilterError::DataTooLarge); + } + Ok(()) } } } @@ -139,14 +134,6 @@ pub struct Memcmp { /// Bytes, encoded with specified encoding, or default Binary #[serde(flatten)] bytes: MemcmpEncodedBytes, - /// Optional encoding specification - #[deprecated( - since = "1.11.2", - note = "Field has no server-side effect. Specify encoding with `MemcmpEncodedBytes` variant instead. \ - Field will be made private in future. Please use a constructor method instead." - )] - #[serde(skip)] - pub encoding: Option, } impl Memcmp { @@ -154,7 +141,6 @@ impl Memcmp { Self { offset, bytes: encoded_bytes, - encoding: None, } } @@ -162,7 +148,6 @@ impl Memcmp { Self { offset, bytes: MemcmpEncodedBytes::Bytes(bytes), - encoding: None, } } @@ -170,7 +155,6 @@ impl Memcmp { Self { offset, bytes: MemcmpEncodedBytes::Base58(bs58::encode(bytes).into_string()), - encoding: None, } } @@ -241,7 +225,6 @@ mod tests { assert!(Memcmp { offset: 0, bytes: MemcmpEncodedBytes::Base58(bs58::encode(vec![1, 2, 3, 4, 5]).into_string()), - encoding: None, } .bytes_match(&data)); @@ -249,7 +232,6 @@ mod tests { assert!(Memcmp { offset: 0, bytes: MemcmpEncodedBytes::Base58(bs58::encode(vec![1, 2]).into_string()), - encoding: None, } .bytes_match(&data)); @@ -257,7 +239,6 @@ mod tests { assert!(Memcmp { offset: 2, bytes: MemcmpEncodedBytes::Base58(bs58::encode(vec![3, 4]).into_string()), - encoding: None, } .bytes_match(&data)); @@ -265,7 +246,6 @@ mod tests { assert!(!Memcmp { offset: 0, bytes: MemcmpEncodedBytes::Base58(bs58::encode(vec![2]).into_string()), - encoding: None, } .bytes_match(&data)); @@ -273,7 +253,6 @@ mod tests { assert!(!Memcmp { offset: 2, bytes: MemcmpEncodedBytes::Base58(bs58::encode(vec![3, 4, 5, 6]).into_string()), - encoding: None, } .bytes_match(&data)); @@ -281,7 +260,6 @@ mod tests { assert!(!Memcmp { offset: 6, bytes: MemcmpEncodedBytes::Base58(bs58::encode(vec![5]).into_string()), - encoding: None, } .bytes_match(&data)); @@ -289,7 +267,6 @@ mod tests { assert!(!Memcmp { offset: 0, bytes: MemcmpEncodedBytes::Base58("III".to_string()), - encoding: None, } .bytes_match(&data)); } @@ -304,7 +281,6 @@ mod tests { RpcFilterType::Memcmp(Memcmp { offset: 0, bytes: MemcmpEncodedBytes::Base58(base58_bytes.to_string()), - encoding: None, }) .verify(), Ok(()) @@ -319,7 +295,6 @@ mod tests { RpcFilterType::Memcmp(Memcmp { offset: 0, bytes: MemcmpEncodedBytes::Base58(base58_bytes.to_string()), - encoding: None, }) .verify(), Err(RpcFilterError::DataTooLarge) From b9db69ba5cf50ee08d2816f5db264fc9f8d451f4 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Mon, 8 Jul 2024 17:01:31 -0600 Subject: [PATCH 05/10] Remove deprecated MemcmpEncodedBytes variant --- rpc-client-api/src/filter.rs | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/rpc-client-api/src/filter.rs b/rpc-client-api/src/filter.rs index ac5668d61678a8..8c5dad2377c090 100644 --- a/rpc-client-api/src/filter.rs +++ b/rpc-client-api/src/filter.rs @@ -25,20 +25,6 @@ impl RpcFilterType { RpcFilterType::Memcmp(compare) => { use MemcmpEncodedBytes::*; match &compare.bytes { - // DEPRECATED - Binary(bytes) => { - if bytes.len() > MAX_DATA_BASE58_SIZE { - return Err(RpcFilterError::Base58DataTooLarge); - } - let bytes = bs58::decode(&bytes) - .into_vec() - .map_err(RpcFilterError::DecodeError)?; - if bytes.len() > MAX_DATA_SIZE { - Err(RpcFilterError::Base58DataTooLarge) - } else { - Ok(()) - } - } Base58(bytes) => { if bytes.len() > MAX_DATA_BASE58_SIZE { return Err(RpcFilterError::DataTooLarge); @@ -108,20 +94,9 @@ pub enum RpcFilterError { Base64DecodeError(#[from] base64::DecodeError), } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum MemcmpEncoding { - Binary, -} - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase", tag = "encoding", content = "bytes")] pub enum MemcmpEncodedBytes { - #[deprecated( - since = "1.8.1", - note = "Please use MemcmpEncodedBytes::Base58 instead" - )] - Binary(String), Base58(String), Base64(String), Bytes(Vec), @@ -131,7 +106,7 @@ pub enum MemcmpEncodedBytes { pub struct Memcmp { /// Data offset to begin match offset: usize, - /// Bytes, encoded with specified encoding, or default Binary + /// Bytes, encoded with specified encoding #[serde(flatten)] bytes: MemcmpEncodedBytes, } @@ -161,7 +136,7 @@ impl Memcmp { pub fn bytes(&self) -> Option>> { use MemcmpEncodedBytes::*; match &self.bytes { - Binary(bytes) | Base58(bytes) => bs58::decode(bytes).into_vec().ok().map(Cow::Owned), + Base58(bytes) => bs58::decode(bytes).into_vec().ok().map(Cow::Owned), Base64(bytes) => base64::decode(bytes).ok().map(Cow::Owned), Bytes(bytes) => Some(Cow::Borrowed(bytes)), } @@ -170,7 +145,7 @@ impl Memcmp { pub fn convert_to_raw_bytes(&mut self) -> Result<(), RpcFilterError> { use MemcmpEncodedBytes::*; match &self.bytes { - Binary(bytes) | Base58(bytes) => { + Base58(bytes) => { let bytes = bs58::decode(bytes).into_vec()?; self.bytes = Bytes(bytes); Ok(()) From c0d0e5a5d7de50cae38035b30de060b09136dca8 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Mon, 8 Jul 2024 17:02:29 -0600 Subject: [PATCH 06/10] Remove deprecated error variants --- rpc-client-api/src/filter.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/rpc-client-api/src/filter.rs b/rpc-client-api/src/filter.rs index 8c5dad2377c090..573821080c1563 100644 --- a/rpc-client-api/src/filter.rs +++ b/rpc-client-api/src/filter.rs @@ -76,18 +76,6 @@ impl RpcFilterType { pub enum RpcFilterError { #[error("encoded binary data should be less than 129 bytes")] DataTooLarge, - #[deprecated( - since = "1.8.1", - note = "Error for MemcmpEncodedBytes::Binary which is deprecated" - )] - #[error("encoded binary (base 58) data should be less than 129 bytes")] - Base58DataTooLarge, - #[deprecated( - since = "1.8.1", - note = "Error for MemcmpEncodedBytes::Binary which is deprecated" - )] - #[error("bs58 decode error")] - DecodeError(bs58::decode::Error), #[error("base58 decode error")] Base58DecodeError(#[from] bs58::decode::Error), #[error("base64 decode error")] From 074c9a333ed18877d2e518228f7dfb3022240912 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Mon, 8 Jul 2024 23:51:56 -0600 Subject: [PATCH 07/10] Add back custom deserialization to support missing 'encoding' field --- rpc-client-api/src/filter.rs | 44 +++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/rpc-client-api/src/filter.rs b/rpc-client-api/src/filter.rs index 573821080c1563..1c763c0db32854 100644 --- a/rpc-client-api/src/filter.rs +++ b/rpc-client-api/src/filter.rs @@ -1,5 +1,6 @@ #![allow(deprecated)] use { + serde::Deserialize, solana_inline_spl::{token::GenericTokenAccount, token_2022::Account}, solana_sdk::account::{AccountSharedData, ReadableAccount}, std::borrow::Cow, @@ -82,7 +83,7 @@ pub enum RpcFilterError { Base64DecodeError(#[from] base64::DecodeError), } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] #[serde(rename_all = "camelCase", tag = "encoding", content = "bytes")] pub enum MemcmpEncodedBytes { Base58(String), @@ -90,6 +91,47 @@ pub enum MemcmpEncodedBytes { Bytes(Vec), } +impl<'de> Deserialize<'de> for MemcmpEncodedBytes { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum DataType { + Encoded(String), + Raw(Vec), + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + enum RpcMemcmpEncoding { + Base58, + Base64, + Bytes, + } + + #[derive(Deserialize)] + struct RpcMemcmpInner { + bytes: DataType, + encoding: Option, + } + + let data = RpcMemcmpInner::deserialize(deserializer)?; + + let memcmp_encoded_bytes = match data.bytes { + DataType::Encoded(bytes) => match data.encoding.unwrap_or(RpcMemcmpEncoding::Base58) { + RpcMemcmpEncoding::Base58 => MemcmpEncodedBytes::Base58(bytes), + RpcMemcmpEncoding::Base64 => MemcmpEncodedBytes::Base64(bytes), + _ => unreachable!(), + }, + DataType::Raw(bytes) => MemcmpEncodedBytes::Bytes(bytes), + }; + + Ok(memcmp_encoded_bytes) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Memcmp { /// Data offset to begin match From c5bbdefde7372f019a353588269f72ee4cd3ff5d Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Tue, 9 Jul 2024 21:14:24 -0600 Subject: [PATCH 08/10] Update tests to demonstrate consistency and changes --- rpc-client-api/src/filter.rs | 79 ++---------------------------------- 1 file changed, 4 insertions(+), 75 deletions(-) diff --git a/rpc-client-api/src/filter.rs b/rpc-client-api/src/filter.rs index 1c763c0db32854..fdadb34d138fdc 100644 --- a/rpc-client-api/src/filter.rs +++ b/rpc-client-api/src/filter.rs @@ -332,20 +332,12 @@ mod tests { Memcmp { offset: OFFSET, bytes: MemcmpEncodedBytes::Base58(BASE58_STR.to_string()), - encoding: None, } ); - // Binary input is deserialized as Base58 - let binary: Memcmp = serde_json::from_str(BINARY_FILTER).unwrap(); - assert_eq!( - binary, - Memcmp { - offset: OFFSET, - bytes: MemcmpEncodedBytes::Base58(BASE58_STR.to_string()), - encoding: None, - } - ); + // Binary input is no longer supported + let binary = serde_json::from_str::(BINARY_FILTER); + assert!(binary.is_err()); // Base58 input let base58_filter: Memcmp = serde_json::from_str(BASE58_FILTER).unwrap(); @@ -354,7 +346,6 @@ mod tests { Memcmp { offset: OFFSET, bytes: MemcmpEncodedBytes::Base58(BASE58_STR.to_string()), - encoding: None, } ); @@ -365,7 +356,6 @@ mod tests { Memcmp { offset: OFFSET, bytes: MemcmpEncodedBytes::Base64(BASE64_STR.to_string()), - encoding: None, } ); @@ -376,7 +366,6 @@ mod tests { Memcmp { offset: OFFSET, bytes: MemcmpEncodedBytes::Bytes(BYTES.to_vec()), - encoding: None, } ); @@ -386,74 +375,16 @@ mod tests { Memcmp { offset: OFFSET, bytes: MemcmpEncodedBytes::Bytes(BYTES.to_vec()), - encoding: None, } ); } #[test] fn test_filter_serialize() { - // Binary variants - let binary_default = Memcmp { - offset: OFFSET, - bytes: MemcmpEncodedBytes::Binary(BASE58_STR.to_string()), - encoding: None, - }; - let serialized_json = json!(binary_default); - assert_eq!( - serialized_json, - serde_json::from_str::(BINARY_FILTER).unwrap() - ); - - let binary_default = Memcmp { - offset: OFFSET, - bytes: MemcmpEncodedBytes::Binary(BASE58_STR.to_string()), - encoding: Some(MemcmpEncoding::Binary), - }; - let serialized_json = json!(binary_default); - assert_eq!( - serialized_json, - serde_json::from_str::(BINARY_FILTER).unwrap() - ); - - let binary_base58 = Memcmp { - offset: OFFSET, - bytes: MemcmpEncodedBytes::Base58(BASE58_STR.to_string()), - encoding: Some(MemcmpEncoding::Binary), - }; - let serialized_json = json!(binary_base58); - assert_eq!( - serialized_json, - serde_json::from_str::(BASE58_FILTER).unwrap() - ); - - let binary_base64 = Memcmp { - offset: OFFSET, - bytes: MemcmpEncodedBytes::Base64(BASE64_STR.to_string()), - encoding: Some(MemcmpEncoding::Binary), - }; - let serialized_json = json!(binary_base64); - assert_eq!( - serialized_json, - serde_json::from_str::(BASE64_FILTER).unwrap() - ); - - let binary_bytes = Memcmp { - offset: OFFSET, - bytes: MemcmpEncodedBytes::Bytes(BYTES.to_vec()), - encoding: Some(MemcmpEncoding::Binary), - }; - let serialized_json = json!(binary_bytes); - assert_eq!( - serialized_json, - serde_json::from_str::(BYTES_FILTER).unwrap() - ); - // Base58 let base58 = Memcmp { offset: OFFSET, bytes: MemcmpEncodedBytes::Base58(BASE58_STR.to_string()), - encoding: None, }; let serialized_json = json!(base58); assert_eq!( @@ -465,7 +396,6 @@ mod tests { let base64 = Memcmp { offset: OFFSET, bytes: MemcmpEncodedBytes::Base64(BASE64_STR.to_string()), - encoding: None, }; let serialized_json = json!(base64); assert_eq!( @@ -477,12 +407,11 @@ mod tests { let bytes = Memcmp { offset: OFFSET, bytes: MemcmpEncodedBytes::Bytes(BYTES.to_vec()), - encoding: None, }; let serialized_json = json!(bytes); assert_eq!( serialized_json, - serde_json::from_str::(BYTES_FILTER).unwrap() + serde_json::from_str::(BYTES_FILTER_WITH_ENCODING).unwrap() ); } } From 93c291ad7b8278690f76f41999c8b26a4661917a Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Mon, 8 Jul 2024 23:59:29 -0600 Subject: [PATCH 09/10] Add helper fns to use in rpc spl-token filter checks --- rpc-client-api/src/filter.rs | 16 +++++++++ rpc/src/rpc.rs | 70 +++++++++++++++--------------------- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/rpc-client-api/src/filter.rs b/rpc-client-api/src/filter.rs index fdadb34d138fdc..9b021db385ef3f 100644 --- a/rpc-client-api/src/filter.rs +++ b/rpc-client-api/src/filter.rs @@ -163,6 +163,10 @@ impl Memcmp { } } + pub fn offset(&self) -> usize { + self.offset + } + pub fn bytes(&self) -> Option>> { use MemcmpEncodedBytes::*; match &self.bytes { @@ -203,6 +207,18 @@ impl Memcmp { None => false, } } + + /// Returns reference to bytes if variant is MemcmpEncodedBytes::Bytes; + /// otherwise returns None. Used exclusively by solana-rpc to check + /// SPL-token filters. + pub fn raw_bytes_as_ref(&self) -> Option<&[u8]> { + use MemcmpEncodedBytes::*; + if let Bytes(bytes) = &self.bytes { + Some(bytes) + } else { + None + } + } } #[cfg(test)] diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index 5a022e5972711b..d62a61ec81fe00 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -39,7 +39,7 @@ use { solana_rpc_client_api::{ config::*, custom_error::RpcCustomError, - filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}, + filter::{Memcmp, RpcFilterType}, request::{ TokenAccountsFilter, DELINQUENT_VALIDATOR_SLOT_DISTANCE, MAX_GET_CONFIRMED_BLOCKS_RANGE, MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS2_LIMIT, @@ -2379,7 +2379,7 @@ fn encode_account( /// Analyze custom filters to determine if the result will be a subset of spl-token accounts by /// owner. /// NOTE: `optimize_filters()` should almost always be called before using this method because of -/// the strict match on `MemcmpEncodedBytes::Bytes`. +/// the requirement that `Memcmp::raw_bytes_as_ref().is_some()`. fn get_spl_token_owner_filter(program_id: &Pubkey, filters: &[RpcFilterType]) -> Option { if !is_known_spl_token_id(program_id) { return None; @@ -2393,28 +2393,21 @@ fn get_spl_token_owner_filter(program_id: &Pubkey, filters: &[RpcFilterType]) -> for filter in filters { match filter { RpcFilterType::DataSize(size) => data_size_filter = Some(*size), - #[allow(deprecated)] - RpcFilterType::Memcmp(Memcmp { - offset, - bytes: MemcmpEncodedBytes::Bytes(bytes), - .. - }) if *offset == account_packed_len && *program_id == token_2022::id() => { - memcmp_filter = Some(bytes) - } - #[allow(deprecated)] - RpcFilterType::Memcmp(Memcmp { - offset, - bytes: MemcmpEncodedBytes::Bytes(bytes), - .. - }) if *offset == SPL_TOKEN_ACCOUNT_OWNER_OFFSET => { - if bytes.len() == PUBKEY_BYTES { - owner_key = Pubkey::try_from(&bytes[..]).ok(); - } else { - incorrect_owner_len = Some(bytes.len()); + RpcFilterType::Memcmp(memcmp) => { + let offset = memcmp.offset(); + if let Some(bytes) = memcmp.raw_bytes_as_ref() { + if offset == account_packed_len && *program_id == token_2022::id() { + memcmp_filter = Some(bytes); + } else if offset == SPL_TOKEN_ACCOUNT_OWNER_OFFSET { + if bytes.len() == PUBKEY_BYTES { + owner_key = Pubkey::try_from(bytes).ok(); + } else { + incorrect_owner_len = Some(bytes.len()); + } + } } } RpcFilterType::TokenAccountState => token_account_state_filter = true, - _ => {} } } if data_size_filter == Some(account_packed_len as u64) @@ -2437,7 +2430,7 @@ fn get_spl_token_owner_filter(program_id: &Pubkey, filters: &[RpcFilterType]) -> /// Analyze custom filters to determine if the result will be a subset of spl-token accounts by /// mint. /// NOTE: `optimize_filters()` should almost always be called before using this method because of -/// the strict match on `MemcmpEncodedBytes::Bytes`. +/// the requirement that `Memcmp::raw_bytes_as_ref().is_some()`. fn get_spl_token_mint_filter(program_id: &Pubkey, filters: &[RpcFilterType]) -> Option { if !is_known_spl_token_id(program_id) { return None; @@ -2451,28 +2444,21 @@ fn get_spl_token_mint_filter(program_id: &Pubkey, filters: &[RpcFilterType]) -> for filter in filters { match filter { RpcFilterType::DataSize(size) => data_size_filter = Some(*size), - #[allow(deprecated)] - RpcFilterType::Memcmp(Memcmp { - offset, - bytes: MemcmpEncodedBytes::Bytes(bytes), - .. - }) if *offset == account_packed_len && *program_id == token_2022::id() => { - memcmp_filter = Some(bytes) - } - #[allow(deprecated)] - RpcFilterType::Memcmp(Memcmp { - offset, - bytes: MemcmpEncodedBytes::Bytes(bytes), - .. - }) if *offset == SPL_TOKEN_ACCOUNT_MINT_OFFSET => { - if bytes.len() == PUBKEY_BYTES { - mint = Pubkey::try_from(&bytes[..]).ok(); - } else { - incorrect_mint_len = Some(bytes.len()); + RpcFilterType::Memcmp(memcmp) => { + let offset = memcmp.offset(); + if let Some(bytes) = memcmp.raw_bytes_as_ref() { + if offset == account_packed_len && *program_id == token_2022::id() { + memcmp_filter = Some(bytes); + } else if offset == SPL_TOKEN_ACCOUNT_MINT_OFFSET { + if bytes.len() == PUBKEY_BYTES { + mint = Pubkey::try_from(bytes).ok(); + } else { + incorrect_mint_len = Some(bytes.len()); + } + } } } RpcFilterType::TokenAccountState => token_account_state_filter = true, - _ => {} } } if data_size_filter == Some(account_packed_len as u64) @@ -4344,7 +4330,7 @@ pub mod tests { JSON_RPC_SERVER_ERROR_TRANSACTION_HISTORY_NOT_AVAILABLE, JSON_RPC_SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION, }, - filter::{Memcmp, MemcmpEncodedBytes}, + filter::MemcmpEncodedBytes, }, solana_runtime::{ accounts_background_service::AbsRequestSender, bank::BankTestConfig, From 9ec60f3cf8a0ad9a0bf759588c2c71ef95db6304 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Tue, 9 Jul 2024 20:04:59 -0600 Subject: [PATCH 10/10] Update base64 syntax and remove allow-deprecated --- rpc-client-api/src/filter.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rpc-client-api/src/filter.rs b/rpc-client-api/src/filter.rs index 9b021db385ef3f..bef8d1d16e8e67 100644 --- a/rpc-client-api/src/filter.rs +++ b/rpc-client-api/src/filter.rs @@ -1,5 +1,5 @@ -#![allow(deprecated)] use { + base64::{prelude::BASE64_STANDARD, Engine}, serde::Deserialize, solana_inline_spl::{token::GenericTokenAccount, token_2022::Account}, solana_sdk::account::{AccountSharedData, ReadableAccount}, @@ -41,7 +41,7 @@ impl RpcFilterType { if bytes.len() > MAX_DATA_BASE64_SIZE { return Err(RpcFilterError::DataTooLarge); } - let bytes = base64::decode(bytes)?; + let bytes = BASE64_STANDARD.decode(bytes)?; if bytes.len() > MAX_DATA_SIZE { Err(RpcFilterError::DataTooLarge) } else { @@ -171,7 +171,7 @@ impl Memcmp { use MemcmpEncodedBytes::*; match &self.bytes { Base58(bytes) => bs58::decode(bytes).into_vec().ok().map(Cow::Owned), - Base64(bytes) => base64::decode(bytes).ok().map(Cow::Owned), + Base64(bytes) => BASE64_STANDARD.decode(bytes).ok().map(Cow::Owned), Bytes(bytes) => Some(Cow::Borrowed(bytes)), } } @@ -185,7 +185,7 @@ impl Memcmp { Ok(()) } Base64(bytes) => { - let bytes = base64::decode(bytes)?; + let bytes = BASE64_STANDARD.decode(bytes)?; self.bytes = Bytes(bytes); Ok(()) } @@ -234,7 +234,7 @@ mod tests { let ff_data = vec![0xffu8; MAX_DATA_SIZE]; let data58 = bs58::encode(&ff_data).into_string(); assert_eq!(data58.len(), MAX_DATA_BASE58_SIZE); - let data64 = base64::encode(&ff_data); + let data64 = BASE64_STANDARD.encode(&ff_data); assert_eq!(data64.len(), MAX_DATA_BASE64_SIZE); }