diff --git a/.github/workflows/pull-request-token.yml b/.github/workflows/pull-request-token.yml index 004016e8..3d3aba03 100644 --- a/.github/workflows/pull-request-token.yml +++ b/.github/workflows/pull-request-token.yml @@ -67,6 +67,14 @@ jobs: ./token/twoxtx-setup.sh ./ci/cargo-test-bpf.sh token/program-2022-test + - name: Build and test token-2022 with "serde" activated + run: | + cargo +"${{ env.RUST_STABLE }}" test \ + --manifest-path=token/program-2022/Cargo.toml \ + --features serde-traits \ + -- --nocapture + exit 0 + - name: Upload programs uses: actions/upload-artifact@v2 with: diff --git a/Cargo.lock b/Cargo.lock index 59cf9add..0a42a58c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,9 +106,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.52" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3" +checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" [[package]] name = "arbitrary" @@ -733,7 +733,7 @@ dependencies = [ "ansi_term", "atty", "bitflags", - "strsim", + "strsim 0.8.0", "textwrap", "unicode-width", "vec_map", @@ -974,6 +974,41 @@ dependencies = [ "zeroize", ] +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2 1.0.36", + "quote 1.0.14", + "strsim 0.10.0", + "syn 1.0.91", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote 1.0.14", + "syn 1.0.91", +] + [[package]] name = "dashmap" version = "4.0.2" @@ -1905,6 +1940,12 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.1.5" @@ -3964,9 +4005,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" dependencies = [ "serde_derive", ] @@ -3982,9 +4023,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" dependencies = [ "proc-macro2 1.0.36", "quote 1.0.14", @@ -3993,9 +4034,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.79" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" dependencies = [ "itoa 1.0.1", "ryu", @@ -4014,6 +4055,28 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling", + "proc-macro2 1.0.36", + "quote 1.0.14", + "syn 1.0.91", +] + [[package]] name = "serde_yaml" version = "0.8.23" @@ -5985,6 +6048,9 @@ dependencies = [ "num-traits", "num_enum", "proptest", + "serde", + "serde_json", + "serde_with", "serial_test", "solana-program", "solana-program-test", @@ -6222,6 +6288,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strum" version = "0.24.0" diff --git a/token/program-2022/Cargo.toml b/token/program-2022/Cargo.toml index bc13d6d1..520b2ad1 100644 --- a/token/program-2022/Cargo.toml +++ b/token/program-2022/Cargo.toml @@ -11,6 +11,7 @@ exclude = ["js/**"] [features] no-entrypoint = [] test-bpf = [] +serde-traits = ["serde", "serde_with"] # Remove these features once the underlying syscalls are released on all networks default = ["zk-ops"] zk-ops = [] @@ -26,6 +27,8 @@ solana-zk-token-sdk = "1.10.29" spl-memo = { version = "3.0.1", path = "../../memo/program", features = [ "no-entrypoint" ] } spl-token = { version = "3.3", path = "../program", features = ["no-entrypoint"] } thiserror = "1.0" +serde = { version = "1.0.136", optional = true } +serde_with = { version = "1.14.0", optional = true } [dev-dependencies] lazy_static = "1.4.0" @@ -33,6 +36,7 @@ proptest = "1.0" serial_test = "0.5.1" solana-program-test = "1.10.29" solana-sdk = "1.10.29" +serde_json = "1.0.81" [lib] crate-type = ["cdylib", "lib"] diff --git a/token/program-2022/src/extension/mod.rs b/token/program-2022/src/extension/mod.rs index 387fc5b1..7c895fa7 100644 --- a/token/program-2022/src/extension/mod.rs +++ b/token/program-2022/src/extension/mod.rs @@ -28,6 +28,9 @@ use { }, }; +#[cfg(feature = "serde-traits")] +use serde::{Deserialize, Serialize}; + /// Confidential Transfer extension pub mod confidential_transfer; /// Default Account State extension @@ -601,6 +604,7 @@ impl Default for AccountType { /// applied to mint accounts, and account extensions must only be applied to token holding /// accounts. #[repr(u16)] +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, TryFromPrimitive, IntoPrimitive)] pub enum ExtensionType { /// Used as padding if the account size would otherwise be 355, same as a multisig diff --git a/token/program-2022/src/extension/transfer_fee/instruction.rs b/token/program-2022/src/extension/transfer_fee/instruction.rs index 85aa548f..5cf442db 100644 --- a/token/program-2022/src/extension/transfer_fee/instruction.rs +++ b/token/program-2022/src/extension/transfer_fee/instruction.rs @@ -9,7 +9,14 @@ use { std::convert::TryFrom, }; +#[cfg(feature = "serde-traits")] +use { + crate::serialization::coption_fromstr, + serde::{Deserialize, Serialize}, +}; + /// Transfer Fee extension instructions +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq)] #[repr(u8)] pub enum TransferFeeInstruction { @@ -27,8 +34,10 @@ pub enum TransferFeeInstruction { /// 0. `[writable]` The mint to initialize. InitializeTransferFeeConfig { /// Pubkey that may update the fees + #[cfg_attr(feature = "serde-traits", serde(with = "coption_fromstr"))] transfer_fee_config_authority: COption, /// Withdraw instructions must be signed by this key + #[cfg_attr(feature = "serde-traits", serde(with = "coption_fromstr"))] withdraw_withheld_authority: COption, /// Amount of transfer collected as fees, expressed as basis points of the /// transfer amount diff --git a/token/program-2022/src/instruction.rs b/token/program-2022/src/instruction.rs index 68c8b476..e948a999 100644 --- a/token/program-2022/src/instruction.rs +++ b/token/program-2022/src/instruction.rs @@ -1,5 +1,7 @@ //! Instruction types +#![allow(deprecated)] // needed to avoid deprecation warning when generating serde implementation for TokenInstruction + use { crate::{ check_program_account, check_spl_token_program_account, @@ -21,6 +23,13 @@ use { }, }; +#[cfg(feature = "serde-traits")] +use { + crate::serialization::coption_fromstr, + serde::{Deserialize, Serialize}, + serde_with::{As, DisplayFromStr}, +}; + /// Minimum number of multisignature signers (min N) pub const MIN_SIGNERS: usize = 1; /// Maximum number of multisignature signers (max N) @@ -32,6 +41,7 @@ const U64_BYTES: usize = 8; /// Instructions supported by the token program. #[repr(C)] +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Debug, PartialEq)] pub enum TokenInstruction<'a> { /// Initializes a new mint and optionally deposits all the newly minted @@ -54,8 +64,10 @@ pub enum TokenInstruction<'a> { /// Number of base 10 digits to the right of the decimal place. decimals: u8, /// The authority/multisignature to mint tokens. + #[cfg_attr(feature = "serde-traits", serde(with = "As::"))] mint_authority: Pubkey, /// The freeze authority/multisignature of the mint. + #[cfg_attr(feature = "serde-traits", serde(with = "coption_fromstr"))] freeze_authority: COption, }, /// Initializes a new account to hold tokens. If this account is associated @@ -180,6 +192,7 @@ pub enum TokenInstruction<'a> { /// The type of authority to update. authority_type: AuthorityType, /// The new authority + #[cfg_attr(feature = "serde-traits", serde(with = "coption_fromstr"))] new_authority: COption, }, /// Mints new tokens to an account. The native mint does not support @@ -444,8 +457,10 @@ pub enum TokenInstruction<'a> { /// Number of base 10 digits to the right of the decimal place. decimals: u8, /// The authority/multisignature to mint tokens. + #[cfg_attr(feature = "serde-traits", serde(with = "As::"))] mint_authority: Pubkey, /// The freeze authority/multisignature of the mint. + #[cfg_attr(feature = "serde-traits", serde(with = "coption_fromstr"))] freeze_authority: COption, }, /// Gets the required size of an account for the given mint as a little-endian @@ -514,6 +529,7 @@ pub enum TokenInstruction<'a> { /// 0. `[writable]` The mint to initialize. InitializeMintCloseAuthority { /// Authority that must sign the `CloseAccount` instruction on a mint + #[cfg_attr(feature = "serde-traits", serde(with = "coption_fromstr"))] close_authority: COption, }, /// The common instruction prefix for Transfer Fee extension instructions. @@ -933,6 +949,7 @@ impl<'a> TokenInstruction<'a> { /// Specifies the authority type for SetAuthority instructions #[repr(u8)] +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Debug, PartialEq)] pub enum AuthorityType { /// Authority to mint new tokens diff --git a/token/program-2022/src/lib.rs b/token/program-2022/src/lib.rs index b36bdb02..48fe4a55 100644 --- a/token/program-2022/src/lib.rs +++ b/token/program-2022/src/lib.rs @@ -10,6 +10,8 @@ pub mod instruction; pub mod native_mint; pub mod pod; pub mod processor; +#[cfg(feature = "serde-traits")] +pub mod serialization; pub mod state; #[cfg(not(feature = "no-entrypoint"))] diff --git a/token/program-2022/src/serialization.rs b/token/program-2022/src/serialization.rs new file mode 100644 index 00000000..9b2d2697 --- /dev/null +++ b/token/program-2022/src/serialization.rs @@ -0,0 +1,78 @@ +//! serialization module + +/// helper function to ser/deser COption wrapped values +pub mod coption_fromstr { + use { + serde::{ + de::{Error, Unexpected, Visitor}, + Deserializer, Serializer, + }, + solana_program::program_option::COption, + std::{ + fmt::{self, Display}, + marker::PhantomData, + str::FromStr, + }, + }; + + /// serialize values supporting Display trait wrapped in COption + pub fn serialize(x: &COption, s: S) -> Result + where + S: Serializer, + T: Display, + { + match *x { + COption::Some(ref value) => s.serialize_some(&value.to_string()), + COption::None => s.serialize_none(), + } + } + + struct COptionVisitor { + s: PhantomData, + } + + impl<'de, T> Visitor<'de> for COptionVisitor + where + T: FromStr, + { + type Value = COption; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a FromStr type") + } + + fn visit_some(self, d: D) -> Result + where + D: Deserializer<'de>, + { + d.deserialize_str(self) + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + T::from_str(v) + .map(|r| COption::Some(r)) + .map_err(|_| E::invalid_value(Unexpected::Str(v), &"value string")) + } + + fn visit_none(self) -> Result + where + E: Error, + { + Ok(COption::None) + } + } + + /// deserialize values supporting Display trait wrapped in COption + pub fn deserialize<'de, D, T>(d: D) -> Result, D::Error> + where + D: Deserializer<'de>, + T: FromStr, + { + d.deserialize_option(COptionVisitor { + s: PhantomData::default(), + }) + } +} diff --git a/token/program-2022/tests/serialization.rs b/token/program-2022/tests/serialization.rs new file mode 100644 index 00000000..46051e9a --- /dev/null +++ b/token/program-2022/tests/serialization.rs @@ -0,0 +1,37 @@ +#![cfg(feature = "serde-traits")] + +use { + solana_program::program_option::COption, solana_sdk::pubkey::Pubkey, + spl_token_2022::instruction, std::str::FromStr, +}; + +#[test] +fn token_program_serde() { + let inst = instruction::TokenInstruction::InitializeMint2 { + decimals: 0, + mint_authority: Pubkey::from_str("4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM").unwrap(), + freeze_authority: COption::Some( + Pubkey::from_str("8opHzTAnfzRpPEx21XtnrVTX28YQuCpAjcn1PczScKh").unwrap(), + ), + }; + + let serialized = serde_json::to_string(&inst).unwrap(); + assert_eq!(&serialized, "{\"InitializeMint2\":{\"decimals\":0,\"mint_authority\":\"4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM\",\"freeze_authority\":\"8opHzTAnfzRpPEx21XtnrVTX28YQuCpAjcn1PczScKh\"}}"); + + serde_json::from_str::(&serialized).unwrap(); +} + +#[test] +fn token_program_serde_with_none() { + let inst = instruction::TokenInstruction::InitializeMintCloseAuthority { + close_authority: COption::None, + }; + + let serialized = serde_json::to_string(&inst).unwrap(); + assert_eq!( + &serialized, + "{\"InitializeMintCloseAuthority\":{\"close_authority\":null}}" + ); + + serde_json::from_str::(&serialized).unwrap(); +}