diff --git a/sdk/program-macros/src/lib.rs b/sdk/program-macros/src/lib.rs index 4b2557e3f2c0dd..6227db3ca60d0d 100644 --- a/sdk/program-macros/src/lib.rs +++ b/sdk/program-macros/src/lib.rs @@ -12,8 +12,8 @@ use syn::{ parse_macro_input, parse_quote, punctuated::Punctuated, token::{Brace, Comma}, - ExprCall, Field, Fields, FieldsNamed, FnArg, Ident, ItemEnum, Lit, Meta, NestedMeta, Path, - Type, + Attribute, ExprCall, Field, Fields, FieldsNamed, FnArg, Ident, ItemEnum, Lit, Meta, NestedMeta, + Path, Type, }; struct ProgramId(ExprCall); @@ -154,18 +154,18 @@ impl AccountDetails { if self.is_optional { quote! { if let Some(#ident) = #ident { - account_metas.push(#account_meta_path(#ident, #is_signer)); + accounts.push(#account_meta_path(#ident, #is_signer)); } } } else if self.allows_multiple { quote! { for pubkey in #ident.into_iter() { - account_metas.push(#account_meta_path(pubkey, #is_signer)); + accounts.push(#account_meta_path(pubkey, #is_signer)); } } } else { quote! { - account_metas.push(#account_meta_path(#ident, #is_signer)); + accounts.push(#account_meta_path(#ident, #is_signer)); } } } @@ -190,11 +190,40 @@ struct VariantDetails { struct ProgramDetails { instruction_enum: ItemEnum, variants: Vec, + serializer: Serializer, +} + +enum Serializer { + Custom, + Serde, +} + +impl Serializer { + fn get(attrs: &[Attribute]) -> Result { + for attr in attrs { + if let Meta::List(list) = attr.parse_meta()? { + if list.path.is_ident("derive") { + for nested_meta in list.nested { + if let NestedMeta::Meta(meta) = nested_meta { + if let Meta::Path(derived) = meta { + if derived.is_ident("Serialize") { + return Ok(Serializer::Serde); + } + } + } + } + } + } + } + Ok(Serializer::Custom) + } } impl Parse for ProgramDetails { fn parse(input: ParseStream) -> Result { let mut instruction_enum = ItemEnum::parse(input)?; + let serializer = Serializer::get(&instruction_enum.attrs)?; + let mut variants: Vec = vec![]; for variant in instruction_enum.variants.iter_mut() { let mut account_details: Vec = vec![]; @@ -229,6 +258,7 @@ impl Parse for ProgramDetails { Ok(ProgramDetails { instruction_enum, variants, + serializer, }) } } @@ -278,12 +308,12 @@ fn build_helper_fns( let mut accounts_stream = proc_macro2::TokenStream::new(); accounts_stream.extend(quote! { - let mut account_metas: Vec<::solana_sdk::instruction::AccountMeta> = vec![]; + let mut accounts: Vec<::solana_sdk::instruction::AccountMeta> = vec![]; }); for account in &variant_details.account_details { - let account_meta = account.format_account_meta(); + let accounts = account.format_account_meta(); accounts_stream.extend(quote! { - #account_meta + #accounts }); args.push(account.format_arg()); @@ -299,14 +329,20 @@ fn build_helper_fns( } } + let data_serialization = match program_details.serializer { + Serializer::Custom => quote!(#ident::#variant_ident{ #fields }.serialize()), + Serializer::Serde => quote!(serialize(&#ident::#variant_ident{ #fields })), + }; + stream.extend( quote!( pub fn #fn_ident( #args ) -> ::solana_sdk::instruction::Instruction { #accounts_stream - ::solana_sdk::instruction::Instruction::new( - #program_id, - &#ident::#variant_ident{ #fields }, - account_metas, - ) + let data = #data_serialization.unwrap(); + ::solana_sdk::instruction::Instruction { + program_id: #program_id, + data, + accounts, + } } ), ); @@ -468,7 +504,7 @@ mod tests { let account_meta = account_details.format_account_meta(); let account_meta: Stmt = parse_quote!(#account_meta); let expected_account_meta: Stmt = parse_quote!( - account_metas.push(::solana_sdk::instruction::AccountMeta::new_readonly(test, false)); + accounts.push(::solana_sdk::instruction::AccountMeta::new_readonly(test, false)); ); assert_eq!(account_meta, expected_account_meta); @@ -476,7 +512,7 @@ mod tests { let account_meta = account_details.format_account_meta(); let account_meta: Stmt = parse_quote!(#account_meta); let expected_account_meta: Stmt = parse_quote!( - account_metas.push(::solana_sdk::instruction::AccountMeta::new_readonly(test, true)); + accounts.push(::solana_sdk::instruction::AccountMeta::new_readonly(test, true)); ); assert_eq!(account_meta, expected_account_meta); @@ -484,7 +520,7 @@ mod tests { let account_meta = account_details.format_account_meta(); let account_meta: Stmt = parse_quote!(#account_meta); let expected_account_meta: Stmt = parse_quote!( - account_metas.push(::solana_sdk::instruction::AccountMeta::new(test, true)); + accounts.push(::solana_sdk::instruction::AccountMeta::new(test, true)); ); assert_eq!(account_meta, expected_account_meta); @@ -492,7 +528,7 @@ mod tests { let account_meta = account_details.format_account_meta(); let account_meta: Stmt = parse_quote!(#account_meta); let expected_account_meta: Stmt = parse_quote!(if let Some(test) = test { - account_metas.push(::solana_sdk::instruction::AccountMeta::new(test, true)); + accounts.push(::solana_sdk::instruction::AccountMeta::new(test, true)); }); assert_eq!(account_meta, expected_account_meta); @@ -501,7 +537,7 @@ mod tests { let account_meta = account_details.format_account_meta(); let account_meta: Stmt = parse_quote!(#account_meta); let expected_account_meta: Stmt = parse_quote!(for pubkey in test.into_iter() { - account_metas.push(::solana_sdk::instruction::AccountMeta::new(pubkey, true)); + accounts.push(::solana_sdk::instruction::AccountMeta::new(pubkey, true)); }); assert_eq!(account_meta, expected_account_meta); } @@ -578,6 +614,7 @@ mod tests { account_details: account_details1, }, ], + serializer: Serializer::Serde, } } @@ -624,37 +661,38 @@ mod tests { another: ::solana_sdk::pubkey::Pubkey, lamports: u64, ) -> ::solana_sdk::instruction::Instruction { - let mut account_metas: Vec<::solana_sdk::instruction::AccountMeta> = vec![]; - account_metas.push( + let mut accounts: Vec<::solana_sdk::instruction::AccountMeta> = vec![]; + accounts.push( ::solana_sdk::instruction::AccountMeta::new(test_account, true) ); if let Some(another) = another { - account_metas.push( + accounts.push( ::solana_sdk::instruction::AccountMeta::new_readonly(another, false) ); } - ::solana_sdk::instruction::Instruction::new( - program::id(), - &TestInstruction::Transfer{lamports,}, - account_metas, - ) + let data = serialize(&TestInstruction::Transfer{lamports,}).unwrap(); + ::solana_sdk::instruction::Instruction { + program_id: program::id(), + data, + accounts, + } } pub fn multiple( signers: Vec<::solana_sdk::pubkey::Pubkey> ) -> ::solana_sdk::instruction::Instruction { - let mut account_metas: Vec<::solana_sdk::instruction::AccountMeta> = vec![]; + let mut accounts: Vec<::solana_sdk::instruction::AccountMeta> = vec![]; for pubkey in signers.into_iter() { - account_metas.push( + accounts.push( ::solana_sdk::instruction::AccountMeta::new_readonly(pubkey, true) ); } - - ::solana_sdk::instruction::Instruction::new( - program::id(), - &TestInstruction::Multiple{}, - account_metas, - ) + let data = serialize(&TestInstruction::Multiple{}).unwrap(); + ::solana_sdk::instruction::Instruction { + program_id: program::id(), + data, + accounts, + } } }; assert_eq!(helper_fns[1], expected_fns[1]); diff --git a/sdk/tests/compile-fail/custom_serde.rs b/sdk/tests/compile-fail/custom_serde.rs new file mode 100644 index 00000000000000..9b4ca11384e9f3 --- /dev/null +++ b/sdk/tests/compile-fail/custom_serde.rs @@ -0,0 +1,14 @@ +use solana_sdk_program_macros::instructions; + +mod test_program; + +#[instructions(test_program::id())] +#[derive(Clone, Debug, PartialEq)] +pub enum TestInstruction { + #[accounts( + from_account(SIGNER, WRITABLE, desc = "Funding account") + )] + Transfer { lamports: u64 }, +} + +fn main() {} diff --git a/sdk/tests/compile-fail/custom_serde.stderr b/sdk/tests/compile-fail/custom_serde.stderr new file mode 100644 index 00000000000000..20c696ac8505ee --- /dev/null +++ b/sdk/tests/compile-fail/custom_serde.stderr @@ -0,0 +1,14 @@ +error[E0599]: no method named `serialize` found for enum `TestInstruction` in the current scope + --> $DIR/custom_serde.rs:5:1 + | +5 | #[instructions(test_program::id())] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ method not found in `TestInstruction` +6 | #[derive(Clone, Debug, PartialEq)] +7 | pub enum TestInstruction { + | ------------------------ method `serialize` not found for this + | + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following traits define an item `serialize`, perhaps you need to implement one of them: + candidate #1: `serde::ser::Serialize` + candidate #2: `serde_bytes::ser::Serialize` + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/sdk/tests/compile_fail.rs b/sdk/tests/compile_fail.rs index b909ddf6fed61b..27bf8e29e98597 100644 --- a/sdk/tests/compile_fail.rs +++ b/sdk/tests/compile_fail.rs @@ -8,5 +8,6 @@ fn compile_test() { t.compile_fail("tests/compile-fail/accounts_list_missing.rs"); t.compile_fail("tests/compile-fail/account_optional_multiple.rs"); t.compile_fail("tests/compile-fail/account_tag.rs"); + t.compile_fail("tests/compile-fail/custom_serde.rs"); t.compile_fail("tests/compile-fail/unnamed_fields.rs"); } diff --git a/sdk/tests/program_macros.rs b/sdk/tests/program_macros.rs index f9be699a9a0e29..3dbe234a812daf 100644 --- a/sdk/tests/program_macros.rs +++ b/sdk/tests/program_macros.rs @@ -94,13 +94,11 @@ fn test_helper_fns() { multiple_accounts(pubkey0, vec![]), Instruction { program_id: test_program::id(), - accounts: vec![ - AccountMeta { - pubkey: pubkey0, - is_signer: false, - is_writable: true, - } - ], + accounts: vec![AccountMeta { + pubkey: pubkey0, + is_signer: false, + is_writable: true, + }], data: serialize(&TestInstruction::MultipleAccounts).unwrap(), } ); diff --git a/sdk/tests/program_macros_custom_serde.rs b/sdk/tests/program_macros_custom_serde.rs new file mode 100644 index 00000000000000..c108752481a112 --- /dev/null +++ b/sdk/tests/program_macros_custom_serde.rs @@ -0,0 +1,196 @@ +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + program_error::ProgramError, + pubkey::Pubkey, +}; +use solana_sdk_program_macros::instructions; + +#[instructions(test_program::id())] +#[derive(Clone, Debug, PartialEq)] +pub enum CustomSerdeInstruction { + /// Transfer lamports + #[accounts( + from_account(SIGNER, WRITABLE, desc = "Funding account"), + to_account(WRITABLE, desc = "Recipient account") + )] + Variant, + + /// Provide one required signature and a variable list of other signatures + #[accounts( + required_account(WRITABLE, desc = "Required account"), + signers(SIGNER, multiple, desc = "Signer") + )] + MultipleAccounts, + + /// Consumes a stored nonce, replacing it with a successor + #[accounts( + required_account(SIGNER, WRITABLE, desc = "Required account"), + sysvar(desc = "Sysvar"), + authority(SIGNER, optional, desc = "Authority") + )] + OptionalAccount, +} + +impl CustomSerdeInstruction { + pub fn serialize(self: &Self) -> Result, ProgramError> { + let mut output = vec![0u8; 1]; + match self { + Self::Variant => output[0] = 0, + Self::MultipleAccounts => output[0] = 1, + Self::OptionalAccount => output[0] = 2, + } + Ok(output) + } +} + +mod test_program { + solana_sdk::declare_id!("8dGutFWpfHymgGDV6is389USqGRqSfpGZyhBrF1VPWDg"); +} + +#[test] +fn test_helper_fns_custom_serde() { + let pubkey0 = Pubkey::new_rand(); + let pubkey1 = Pubkey::new_rand(); + let pubkey2 = Pubkey::new_rand(); + + assert_eq!( + variant(pubkey0, pubkey1), + Instruction { + program_id: test_program::id(), + accounts: vec![ + AccountMeta { + pubkey: pubkey0, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: pubkey1, + is_signer: false, + is_writable: true, + } + ], + data: CustomSerdeInstruction::Variant.serialize().unwrap(), + } + ); + + assert_eq!( + multiple_accounts(pubkey0, vec![pubkey1, pubkey2]), + Instruction { + program_id: test_program::id(), + accounts: vec![ + AccountMeta { + pubkey: pubkey0, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: pubkey1, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: pubkey2, + is_signer: true, + is_writable: false, + } + ], + data: CustomSerdeInstruction::MultipleAccounts + .serialize() + .unwrap(), + } + ); + + assert_eq!( + multiple_accounts(pubkey0, vec![]), + Instruction { + program_id: test_program::id(), + accounts: vec![AccountMeta { + pubkey: pubkey0, + is_signer: false, + is_writable: true, + }], + data: CustomSerdeInstruction::MultipleAccounts + .serialize() + .unwrap(), + } + ); + + assert_eq!( + optional_account(pubkey0, pubkey1, Some(pubkey2)), + Instruction { + program_id: test_program::id(), + accounts: vec![ + AccountMeta { + pubkey: pubkey0, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: pubkey1, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: pubkey2, + is_signer: true, + is_writable: false, + } + ], + data: CustomSerdeInstruction::OptionalAccount.serialize().unwrap(), + } + ); + + assert_eq!( + optional_account(pubkey0, pubkey1, None), + Instruction { + program_id: test_program::id(), + accounts: vec![ + AccountMeta { + pubkey: pubkey0, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: pubkey1, + is_signer: false, + is_writable: false, + } + ], + data: CustomSerdeInstruction::OptionalAccount.serialize().unwrap(), + } + ); +} + +#[test] +fn test_from_instruction_custom_serde() { + let transfer = CustomSerdeInstruction::Variant; + let verbose_transfer = CustomSerdeInstructionVerbose::from_instruction(transfer, vec![2, 3]); + assert_eq!( + verbose_transfer, + CustomSerdeInstructionVerbose::Variant { + from_account: 2, + to_account: 3, + } + ); + + let multiple = CustomSerdeInstruction::MultipleAccounts; + let verbose_multiple = CustomSerdeInstructionVerbose::from_instruction(multiple, vec![2, 3, 4]); + assert_eq!( + verbose_multiple, + CustomSerdeInstructionVerbose::MultipleAccounts { + required_account: 2, + signers: vec![3, 4], + } + ); + + let optional = CustomSerdeInstruction::OptionalAccount; + let verbose_optional = CustomSerdeInstructionVerbose::from_instruction(optional, vec![2, 3, 4]); + assert_eq!( + verbose_optional, + CustomSerdeInstructionVerbose::OptionalAccount { + required_account: 2, + sysvar: 3, + authority: Some(4), + } + ); +}