From 46aef02e673e47343d535e0d0c801899ae7b4de3 Mon Sep 17 00:00:00 2001 From: serbangv Date: Wed, 16 Aug 2023 17:04:16 +0300 Subject: [PATCH] [token-2022] Add serde support for the rest of token-2022 instructions (#4772) * token-2022: Add serde support for the rest of token-2022 instructions #3325 * token-2022: Add serde support for the rest of token-2022 instructions #3325 * revert ts file changes * token-2022: Add serde support for the rest of token-2022 instructions #3325 * token-2022: Add serde support for the rest of token-2022 instructions #3325 * token-2022: Add serde support for the rest of token-2022 instructions #3325 * token-2022: Add serde support for the rest of token-2022 instructions #3325 --------- Co-authored-by: un Co-authored-by: Serban <@> --- Cargo.lock | 1 + token/program-2022/Cargo.toml | 3 +- .../confidential_transfer/instruction.rs | 19 ++ .../confidential_transfer_fee/instruction.rs | 13 ++ .../src/extension/cpi_guard/instruction.rs | 4 + .../default_account_state/instruction.rs | 4 + .../interest_bearing_mint/instruction.rs | 5 + .../extension/memo_transfer/instruction.rs | 4 + .../extension/metadata_pointer/instruction.rs | 6 + .../extension/transfer_hook/instruction.rs | 6 + token/program-2022/src/instruction.rs | 3 + token/program-2022/src/pod.rs | 63 ++++++ token/program-2022/src/serialization.rs | 187 +++++++++++++++++- token/program-2022/tests/serialization.rs | 136 ++++++++++++- 14 files changed, 448 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e6460e242e..e8934f432ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6984,6 +6984,7 @@ name = "spl-token-2022" version = "0.7.0" dependencies = [ "arrayref", + "base64 0.21.2", "bytemuck", "lazy_static", "num-derive 0.4.0", diff --git a/token/program-2022/Cargo.toml b/token/program-2022/Cargo.toml index d23753bf8ef..318dee7e315 100644 --- a/token/program-2022/Cargo.toml +++ b/token/program-2022/Cargo.toml @@ -11,7 +11,7 @@ exclude = ["js/**"] [features] no-entrypoint = [] test-sbf = [] -serde-traits = ["serde", "serde_with"] +serde-traits = ["serde", "serde_with", "base64"] # Remove these features once the underlying syscalls are released on all networks default = ["zk-ops"] zk-ops = [] @@ -33,6 +33,7 @@ spl-type-length-value = { version = "0.2.0", path = "../../libraries/type-length thiserror = "1.0" serde = { version = "1.0.183", optional = true } serde_with = { version = "3.2.0", optional = true } +base64 = { version = "0.21.2", optional = true } [dev-dependencies] lazy_static = "1.4.0" diff --git a/token/program-2022/src/extension/confidential_transfer/instruction.rs b/token/program-2022/src/extension/confidential_transfer/instruction.rs index 60261475c21..989c1c00e47 100644 --- a/token/program-2022/src/extension/confidential_transfer/instruction.rs +++ b/token/program-2022/src/extension/confidential_transfer/instruction.rs @@ -20,7 +20,14 @@ use { }, }; +#[cfg(feature = "serde-traits")] +use { + crate::serialization::aeciphertext_fromstr, + serde::{Deserialize, Serialize}, +}; + /// Confidential Transfer extension instructions +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)] #[repr(u8)] pub enum ConfidentialTransferInstruction { @@ -352,6 +359,7 @@ pub enum ConfidentialTransferInstruction { } /// Data expected by `ConfidentialTransferInstruction::InitializeMint` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] #[repr(C)] pub struct InitializeMintData { @@ -366,6 +374,7 @@ pub struct InitializeMintData { } /// Data expected by `ConfidentialTransferInstruction::UpdateMint` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] #[repr(C)] pub struct UpdateMintData { @@ -377,10 +386,12 @@ pub struct UpdateMintData { } /// Data expected by `ConfidentialTransferInstruction::ConfigureAccount` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] #[repr(C)] pub struct ConfigureAccountInstructionData { /// The decryptable balance (always 0) once the configure account succeeds + #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] pub decryptable_zero_balance: DecryptableBalance, /// The maximum number of despots and transfers that an account can receiver before the /// `ApplyPendingBalance` is executed @@ -392,6 +403,7 @@ pub struct ConfigureAccountInstructionData { } /// Data expected by `ConfidentialTransferInstruction::EmptyAccount` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] #[repr(C)] pub struct EmptyAccountInstructionData { @@ -402,6 +414,7 @@ pub struct EmptyAccountInstructionData { } /// Data expected by `ConfidentialTransferInstruction::Deposit` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] #[repr(C)] pub struct DepositInstructionData { @@ -412,6 +425,7 @@ pub struct DepositInstructionData { } /// Data expected by `ConfidentialTransferInstruction::Withdraw` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] #[repr(C)] pub struct WithdrawInstructionData { @@ -420,6 +434,7 @@ pub struct WithdrawInstructionData { /// Expected number of base 10 digits to the right of the decimal place pub decimals: u8, /// The new decryptable balance if the withdrawal succeeds + #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] pub new_decryptable_available_balance: DecryptableBalance, /// Relative location of the `ProofInstruction::VerifyWithdraw` instruction to the `Withdraw` /// instruction in the transaction. If the offset is `0`, then use a context state account for @@ -428,10 +443,12 @@ pub struct WithdrawInstructionData { } /// Data expected by `ConfidentialTransferInstruction::Transfer` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] #[repr(C)] pub struct TransferInstructionData { /// The new source decryptable balance if the transfer succeeds + #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] pub new_source_decryptable_available_balance: DecryptableBalance, /// Relative location of the `ProofInstruction::VerifyTransfer` instruction to the /// `Transfer` instruction in the transaction. If the offset is `0`, then use a context state @@ -440,6 +457,7 @@ pub struct TransferInstructionData { } /// Data expected by `ConfidentialTransferInstruction::ApplyPendingBalance` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] #[repr(C)] pub struct ApplyPendingBalanceData { @@ -447,6 +465,7 @@ pub struct ApplyPendingBalanceData { /// `ApplyPendingBalance` instruction pub expected_pending_balance_credit_counter: PodU64, /// The new decryptable balance if the pending balance is applied successfully + #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] pub new_decryptable_available_balance: DecryptableBalance, } diff --git a/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs b/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs index 62739a59614..650405179c2 100644 --- a/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs +++ b/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs @@ -24,7 +24,14 @@ use { std::convert::TryFrom, }; +#[cfg(feature = "serde-traits")] +use { + crate::serialization::{aeciphertext_fromstr, elgamalpubkey_fromstr}, + serde::{Deserialize, Serialize}, +}; + /// Confidential Transfer extension instructions +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)] #[repr(u8)] pub enum ConfidentialTransferFeeInstruction { @@ -190,6 +197,7 @@ pub enum ConfidentialTransferFeeInstruction { } /// Data expected by `InitializeConfidentialTransferFeeConfig` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] #[repr(C)] pub struct InitializeConfidentialTransferFeeConfigData { @@ -197,10 +205,12 @@ pub struct InitializeConfidentialTransferFeeConfigData { pub authority: OptionalNonZeroPubkey, /// ElGamal public key used to encrypt withheld fees. + #[cfg_attr(feature = "serde-traits", serde(with = "elgamalpubkey_fromstr"))] pub withdraw_withheld_authority_elgamal_pubkey: ElGamalPubkey, } /// Data expected by `ConfidentialTransferFeeInstruction::WithdrawWithheldTokensFromMint` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] #[repr(C)] pub struct WithdrawWithheldTokensFromMintData { @@ -209,10 +219,12 @@ pub struct WithdrawWithheldTokensFromMintData { /// use a context state account for the proof. pub proof_instruction_offset: i8, /// The new decryptable balance in the destination token account. + #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] pub new_decryptable_available_balance: DecryptableBalance, } /// Data expected by `ConfidentialTransferFeeInstruction::WithdrawWithheldTokensFromAccounts` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] #[repr(C)] pub struct WithdrawWithheldTokensFromAccountsData { @@ -223,6 +235,7 @@ pub struct WithdrawWithheldTokensFromAccountsData { /// `0`, then use a context state account for the proof. pub proof_instruction_offset: i8, /// The new decryptable balance in the destination token account. + #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] pub new_decryptable_available_balance: DecryptableBalance, } diff --git a/token/program-2022/src/extension/cpi_guard/instruction.rs b/token/program-2022/src/extension/cpi_guard/instruction.rs index 9570315eb9f..ff0bd432b6d 100644 --- a/token/program-2022/src/extension/cpi_guard/instruction.rs +++ b/token/program-2022/src/extension/cpi_guard/instruction.rs @@ -11,7 +11,11 @@ use { }, }; +#[cfg(feature = "serde-traits")] +use serde::{Deserialize, Serialize}; + /// CPI Guard extension instructions +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] #[repr(u8)] pub enum CpiGuardInstruction { diff --git a/token/program-2022/src/extension/default_account_state/instruction.rs b/token/program-2022/src/extension/default_account_state/instruction.rs index c872f41e89e..f515a7925f5 100644 --- a/token/program-2022/src/extension/default_account_state/instruction.rs +++ b/token/program-2022/src/extension/default_account_state/instruction.rs @@ -12,7 +12,11 @@ use { std::convert::TryFrom, }; +#[cfg(feature = "serde-traits")] +use serde::{Deserialize, Serialize}; + /// Default Account State extension instructions +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] #[repr(u8)] pub enum DefaultAccountStateInstruction { diff --git a/token/program-2022/src/extension/interest_bearing_mint/instruction.rs b/token/program-2022/src/extension/interest_bearing_mint/instruction.rs index 5231bb959e0..f8a4b64ce70 100644 --- a/token/program-2022/src/extension/interest_bearing_mint/instruction.rs +++ b/token/program-2022/src/extension/interest_bearing_mint/instruction.rs @@ -15,7 +15,11 @@ use { std::convert::TryInto, }; +#[cfg(feature = "serde-traits")] +use serde::{Deserialize, Serialize}; + /// Interesting-bearing mint extension instructions +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] #[repr(u8)] pub enum InterestBearingMintInstruction { @@ -57,6 +61,7 @@ pub enum InterestBearingMintInstruction { } /// Data expected by `InterestBearing::Initialize` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct InitializeInstructionData { diff --git a/token/program-2022/src/extension/memo_transfer/instruction.rs b/token/program-2022/src/extension/memo_transfer/instruction.rs index f299167d27f..d25c95ace7b 100644 --- a/token/program-2022/src/extension/memo_transfer/instruction.rs +++ b/token/program-2022/src/extension/memo_transfer/instruction.rs @@ -11,7 +11,11 @@ use { }, }; +#[cfg(feature = "serde-traits")] +use serde::{Deserialize, Serialize}; + /// Required Memo Transfers extension instructions +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] #[repr(u8)] pub enum RequiredMemoTransfersInstruction { diff --git a/token/program-2022/src/extension/metadata_pointer/instruction.rs b/token/program-2022/src/extension/metadata_pointer/instruction.rs index e379bfa3af2..e064ffed47a 100644 --- a/token/program-2022/src/extension/metadata_pointer/instruction.rs +++ b/token/program-2022/src/extension/metadata_pointer/instruction.rs @@ -14,7 +14,11 @@ use { std::convert::TryInto, }; +#[cfg(feature = "serde-traits")] +use serde::{Deserialize, Serialize}; + /// Metadata pointer extension instructions +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] #[repr(u8)] pub enum MetadataPointerInstruction { @@ -56,6 +60,7 @@ pub enum MetadataPointerInstruction { } /// Data expected by `Initialize` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct InitializeInstructionData { @@ -66,6 +71,7 @@ pub struct InitializeInstructionData { } /// Data expected by `Update` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct UpdateInstructionData { diff --git a/token/program-2022/src/extension/transfer_hook/instruction.rs b/token/program-2022/src/extension/transfer_hook/instruction.rs index 902270bdf80..85bb46ddd0a 100644 --- a/token/program-2022/src/extension/transfer_hook/instruction.rs +++ b/token/program-2022/src/extension/transfer_hook/instruction.rs @@ -14,7 +14,11 @@ use { std::convert::TryInto, }; +#[cfg(feature = "serde-traits")] +use serde::{Deserialize, Serialize}; + /// Transfer hook extension instructions +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] #[repr(u8)] pub enum TransferHookInstruction { @@ -56,6 +60,7 @@ pub enum TransferHookInstruction { } /// Data expected by `Initialize` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct InitializeInstructionData { @@ -66,6 +71,7 @@ pub struct InitializeInstructionData { } /// Data expected by `Update` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct UpdateInstructionData { diff --git a/token/program-2022/src/instruction.rs b/token/program-2022/src/instruction.rs index ddd56fc8447..6d73e3423d7 100644 --- a/token/program-2022/src/instruction.rs +++ b/token/program-2022/src/instruction.rs @@ -420,6 +420,7 @@ pub enum TokenInstruction<'a> { /// 2. `[]` Rent sysvar InitializeAccount2 { /// The new account's owner/multisignature. + #[cfg_attr(feature = "serde-traits", serde(with = "As::"))] owner: Pubkey, }, /// Given a wrapped / native token account (a token account containing SOL) @@ -440,6 +441,7 @@ pub enum TokenInstruction<'a> { /// 1. `[]` The mint this account will be associated with. InitializeAccount3 { /// The new account's owner/multisignature. + #[cfg_attr(feature = "serde-traits", serde(with = "As::"))] owner: Pubkey, }, /// Like InitializeMultisig, but does not require the Rent sysvar to be provided @@ -635,6 +637,7 @@ pub enum TokenInstruction<'a> { /// InitializePermanentDelegate { /// Authority that may sign for `Transfer`s and `Burn`s on any account + #[cfg_attr(feature = "serde-traits", serde(with = "As::"))] delegate: Pubkey, }, /// The common instruction prefix for transfer hook extension instructions. diff --git a/token/program-2022/src/pod.rs b/token/program-2022/src/pod.rs index 88691b1a562..83678617a9f 100644 --- a/token/program-2022/src/pod.rs +++ b/token/program-2022/src/pod.rs @@ -6,6 +6,14 @@ use { std::convert::TryFrom, }; +#[cfg(feature = "serde-traits")] +use { + crate::serialization::visitors::{ + OptionalNonZeroElGamalPubkeyVisitor, OptionalNonZeroPubkeyVisitor, + }, + serde::{Deserialize, Deserializer, Serialize, Serializer}, +}; + /// A Pubkey that encodes `None` as all `0`, meant to be usable as a Pod type, /// similar to all NonZero* number types from the bytemuck library. #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] @@ -60,6 +68,30 @@ impl From for COption { } } +#[cfg(feature = "serde-traits")] +impl Serialize for OptionalNonZeroPubkey { + fn serialize(&self, s: S) -> Result + where + S: Serializer, + { + if self.0 == Pubkey::default() { + s.serialize_none() + } else { + s.serialize_some(&self.0.to_string()) + } + } +} + +#[cfg(feature = "serde-traits")] +impl<'de> Deserialize<'de> for OptionalNonZeroPubkey { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(OptionalNonZeroPubkeyVisitor) + } +} + /// An ElGamalPubkey that encodes `None` as all `0`, meant to be usable as a Pod type. #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] #[repr(transparent)] @@ -95,16 +127,43 @@ impl From for Option { } } } +#[cfg(feature = "serde-traits")] +impl Serialize for OptionalNonZeroElGamalPubkey { + fn serialize(&self, s: S) -> Result + where + S: Serializer, + { + if self.0 == ElGamalPubkey::default() { + s.serialize_none() + } else { + s.serialize_some(&self.0.to_string()) + } + } +} + +#[cfg(feature = "serde-traits")] +impl<'de> Deserialize<'de> for OptionalNonZeroElGamalPubkey { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(OptionalNonZeroElGamalPubkeyVisitor) + } +} /// The standard `bool` is not a `Pod`, define a replacement that is +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(from = "bool", into = "bool"))] #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] #[repr(transparent)] pub struct PodBool(u8); + impl From for PodBool { fn from(b: bool) -> Self { Self(if b { 1 } else { 0 }) } } + impl From<&PodBool> for bool { fn from(b: &PodBool) -> Self { b.0 != 0 @@ -143,12 +202,16 @@ pub struct PodU16([u8; 2]); impl_int_conversion!(PodU16, u16); /// `i16` type that can be used in `Pod`s +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(from = "i16", into = "i16"))] #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] #[repr(transparent)] pub struct PodI16([u8; 2]); impl_int_conversion!(PodI16, i16); /// `u64` type that can be used in `Pod`s +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(from = "u64", into = "u64"))] #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] #[repr(transparent)] pub struct PodU64([u8; 8]); diff --git a/token/program-2022/src/serialization.rs b/token/program-2022/src/serialization.rs index 9b2d269748c..e76d3c72341 100644 --- a/token/program-2022/src/serialization.rs +++ b/token/program-2022/src/serialization.rs @@ -1,4 +1,25 @@ -//! serialization module +//! serialization module - contains helpers for serde types from other crates, deserialization visitors + +use { + base64::{prelude::BASE64_STANDARD, Engine}, + serde::de::Error, +}; + +/// helper function to convert base64 encoded string into a bytes array +fn base64_to_bytes(v: &str) -> Result<[u8; N], E> { + let bytes = BASE64_STANDARD.decode(v).map_err(Error::custom)?; + + if bytes.len() != N { + return Err(Error::custom(format!( + "Length of base64 decoded bytes is not {}", + N + ))); + } + + let mut array = [0; N]; + array.copy_from_slice(&bytes[0..N]); + Ok(array) +} /// helper function to ser/deser COption wrapped values pub mod coption_fromstr { @@ -76,3 +97,167 @@ pub mod coption_fromstr { }) } } + +/// helper to ser/deser AeCiphertext values +pub mod aeciphertext_fromstr { + use { + serde::{ + de::{Error, Visitor}, + Deserializer, Serializer, + }, + solana_zk_token_sdk::zk_token_elgamal::pod::AeCiphertext, + std::fmt, + }; + + const AE_CIPHERTEXT_LEN: usize = 36; + + /// serialize AeCiphertext values supporting Display trait + pub fn serialize(x: &AeCiphertext, s: S) -> Result + where + S: Serializer, + { + s.serialize_str(&x.to_string()) + } + + struct AeCiphertextVisitor; + + impl<'de> Visitor<'de> for AeCiphertextVisitor { + type Value = AeCiphertext; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a FromStr type") + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + let array = super::base64_to_bytes::(v)?; + Ok(AeCiphertext(array)) + } + } + + /// deserialize AeCiphertext values from str + pub fn deserialize<'de, D>(d: D) -> Result + where + D: Deserializer<'de>, + { + d.deserialize_str(AeCiphertextVisitor) + } +} + +/// helper to ser/deser pod::ElGamalPubkey values +pub mod elgamalpubkey_fromstr { + use { + serde::{ + de::{Error, Visitor}, + Deserializer, Serializer, + }, + solana_zk_token_sdk::zk_token_elgamal::pod::ElGamalPubkey, + std::fmt, + }; + + const ELGAMAL_PUBKEY_LEN: usize = 32; + + /// serialize ElGamalPubkey values supporting Display trait + pub fn serialize(x: &ElGamalPubkey, s: S) -> Result + where + S: Serializer, + { + s.serialize_str(&x.to_string()) + } + + struct ElGamalPubkeyVisitor; + + impl<'de> Visitor<'de> for ElGamalPubkeyVisitor { + type Value = ElGamalPubkey; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a FromStr type") + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + let array = super::base64_to_bytes::(v)?; + Ok(ElGamalPubkey(array)) + } + } + + /// deserialize ElGamalPubkey values from str + pub fn deserialize<'de, D>(d: D) -> Result + where + D: Deserializer<'de>, + { + d.deserialize_str(ElGamalPubkeyVisitor) + } +} + +/// deserialization Visitors for local types +pub mod visitors { + use { + crate::{pod::OptionalNonZeroElGamalPubkey, pod::OptionalNonZeroPubkey}, + serde::de::{Error, Unexpected, Visitor}, + solana_program::pubkey::Pubkey, + solana_zk_token_sdk::zk_token_elgamal::pod::ElGamalPubkey, + std::{convert::TryFrom, fmt, str::FromStr}, + }; + + /// Visitor for deserializing OptionalNonZeroPubkey + pub(crate) struct OptionalNonZeroPubkeyVisitor; + + impl<'de> Visitor<'de> for OptionalNonZeroPubkeyVisitor { + type Value = OptionalNonZeroPubkey; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a Pubkey in base58 or `null`") + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + let pkey = Pubkey::from_str(&v) + .map_err(|_| Error::invalid_value(Unexpected::Str(v), &"value string"))?; + + OptionalNonZeroPubkey::try_from(Some(pkey)) + .map_err(|_| Error::custom("Failed to convert from pubkey")) + } + + fn visit_unit(self) -> Result + where + E: Error, + { + OptionalNonZeroPubkey::try_from(None).map_err(|e| Error::custom(e.to_string())) + } + } + + /// Visitor for deserializing OptionalNonZeroElGamalPubkey + pub(crate) struct OptionalNonZeroElGamalPubkeyVisitor; + const OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN: usize = 32; + + impl<'de> Visitor<'de> for OptionalNonZeroElGamalPubkeyVisitor { + type Value = OptionalNonZeroElGamalPubkey; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an ElGamal public key as base64 or `null`") + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + let array = super::base64_to_bytes::(v)?; + let elgamal_pubkey = ElGamalPubkey(array); + OptionalNonZeroElGamalPubkey::try_from(Some(elgamal_pubkey)).map_err(Error::custom) + } + + fn visit_unit(self) -> Result + where + E: Error, + { + Ok(OptionalNonZeroElGamalPubkey::default()) + } + } +} diff --git a/token/program-2022/tests/serialization.rs b/token/program-2022/tests/serialization.rs index 46051e9ac5c..9a23ebd3074 100644 --- a/token/program-2022/tests/serialization.rs +++ b/token/program-2022/tests/serialization.rs @@ -1,12 +1,19 @@ #![cfg(feature = "serde-traits")] use { - solana_program::program_option::COption, solana_sdk::pubkey::Pubkey, - spl_token_2022::instruction, std::str::FromStr, + solana_program::program_option::COption, + solana_sdk::pubkey::Pubkey, + spl_token_2022::{ + extension::confidential_transfer, + instruction, + pod::{OptionalNonZeroElGamalPubkey, OptionalNonZeroPubkey}, + solana_zk_token_sdk::zk_token_elgamal::pod::{AeCiphertext, ElGamalPubkey}, + }, + std::str::FromStr, }; #[test] -fn token_program_serde() { +fn serde_instruction_coption_pubkey() { let inst = instruction::TokenInstruction::InitializeMint2 { decimals: 0, mint_authority: Pubkey::from_str("4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM").unwrap(), @@ -22,7 +29,7 @@ fn token_program_serde() { } #[test] -fn token_program_serde_with_none() { +fn serde_instruction_coption_pubkey_with_none() { let inst = instruction::TokenInstruction::InitializeMintCloseAuthority { close_authority: COption::None, }; @@ -35,3 +42,124 @@ fn token_program_serde_with_none() { serde_json::from_str::(&serialized).unwrap(); } + +#[test] +fn serde_instruction_optional_nonzero_pubkeys_podbool() { + // tests serde of ix containing OptionalNonZeroPubkey, PodBool and OptionalNonZeroElGamalPubkey + let authority_option: Option = + Some(Pubkey::from_str("4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM").unwrap()); + let authority: OptionalNonZeroPubkey = authority_option.try_into().unwrap(); + + let elgamal_pubkey_pod_option: Option = Some(ElGamalPubkey([ + 162, 23, 108, 36, 130, 143, 18, 219, 196, 134, 242, 145, 179, 49, 229, 193, 74, 64, 3, 158, + 68, 235, 124, 88, 247, 144, 164, 254, 228, 12, 173, 85, + ])); + let auditor_elgamal_pubkey: OptionalNonZeroElGamalPubkey = + elgamal_pubkey_pod_option.try_into().unwrap(); + + let inst = confidential_transfer::instruction::InitializeMintData { + authority, + auto_approve_new_accounts: false.into(), + auditor_elgamal_pubkey, + }; + + let serialized = serde_json::to_string(&inst).unwrap(); + let serialized_expected = &format!("{{\"authority\":\"4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM\",\"auto_approve_new_accounts\":false,\"auditor_elgamal_pubkey\":\"ohdsJIKPEtvEhvKRszHlwUpAA55E63xY95Ck/uQMrVU=\"}}"); + assert_eq!(&serialized, serialized_expected); + + let deserialized = + serde_json::from_str::(&serialized) + .unwrap(); + assert_eq!(inst, deserialized); +} + +#[test] +fn serde_instruction_optional_nonzero_pubkeys_podbool_with_none() { + // tests serde of ix containing OptionalNonZeroPubkey, PodBool and OptionalNonZeroElGamalPubkey + // with null values + let authority: OptionalNonZeroPubkey = None.try_into().unwrap(); + + let auditor_elgamal_pubkey: OptionalNonZeroElGamalPubkey = + OptionalNonZeroElGamalPubkey::default(); + + let inst = confidential_transfer::instruction::InitializeMintData { + authority, + auto_approve_new_accounts: false.into(), + auditor_elgamal_pubkey, + }; + + let serialized = serde_json::to_string(&inst).unwrap(); + let serialized_expected = &format!("{{\"authority\":null,\"auto_approve_new_accounts\":false,\"auditor_elgamal_pubkey\":null}}"); + assert_eq!(&serialized, serialized_expected); + + let deserialized = + serde_json::from_str::( + &serialized_expected, + ) + .unwrap(); + assert_eq!(inst, deserialized); +} + +#[test] +fn serde_instruction_decryptable_balance_podu64() { + let decryptable_zero_balance = AeCiphertext([ + 56, 22, 102, 48, 112, 106, 58, 25, 25, 244, 194, 217, 73, 137, 73, 38, 24, 26, 36, 25, 235, + 234, 68, 181, 11, 82, 170, 163, 89, 205, 113, 160, 55, 16, 35, 151, + ]); + + let inst = confidential_transfer::instruction::ConfigureAccountInstructionData { + decryptable_zero_balance, + maximum_pending_balance_credit_counter: 1099.into(), + proof_instruction_offset: 100, + }; + + let serialized = serde_json::to_string(&inst).unwrap(); + let serialized_expected = &format!("{{\"decryptable_zero_balance\":\"OBZmMHBqOhkZ9MLZSYlJJhgaJBnr6kS1C1Kqo1nNcaA3ECOX\",\"maximum_pending_balance_credit_counter\":1099,\"proof_instruction_offset\":100}}"); + assert_eq!(&serialized, serialized_expected); + + let deserialized = serde_json::from_str::< + confidential_transfer::instruction::ConfigureAccountInstructionData, + >(&serialized_expected) + .unwrap(); + assert_eq!(inst, deserialized); +} + +#[test] +fn serde_instruction_elgamal_pubkey() { + use spl_token_2022::extension::confidential_transfer_fee::instruction::InitializeConfidentialTransferFeeConfigData; + + let withdraw_withheld_authority_elgamal_pubkey = ElGamalPubkey([ + 162, 23, 108, 36, 130, 143, 18, 219, 196, 134, 242, 145, 179, 49, 229, 193, 74, 64, 3, 158, + 68, 235, 124, 88, 247, 144, 164, 254, 228, 12, 173, 85, + ]); + + let inst = InitializeConfidentialTransferFeeConfigData { + authority: OptionalNonZeroPubkey::default(), + withdraw_withheld_authority_elgamal_pubkey, + }; + + let serialized = serde_json::to_string(&inst).unwrap(); + let serialized_expected = "{\"authority\":null,\"withdraw_withheld_authority_elgamal_pubkey\":\"ohdsJIKPEtvEhvKRszHlwUpAA55E63xY95Ck/uQMrVU=\"}"; + assert_eq!(&serialized, serialized_expected); + + let deserialized = + serde_json::from_str::(&serialized_expected) + .unwrap(); + assert_eq!(inst, deserialized); +} + +#[test] +fn serde_instruction_basis_points() { + use spl_token_2022::extension::interest_bearing_mint::instruction::InitializeInstructionData; + + let inst = InitializeInstructionData { + rate_authority: OptionalNonZeroPubkey::default(), + rate: 127.into(), + }; + + let serialized = serde_json::to_string(&inst).unwrap(); + let serialized_expected = "{\"rate_authority\":null,\"rate\":127}"; + assert_eq!(&serialized, serialized_expected); + + serde_json::from_str::(&serialized_expected).unwrap(); +}