diff --git a/shank-idl/tests/fixtures/instructions/single_file/create_idl_instructions.json b/shank-idl/tests/fixtures/instructions/single_file/create_idl_instructions.json new file mode 100644 index 0000000..47c53e7 --- /dev/null +++ b/shank-idl/tests/fixtures/instructions/single_file/create_idl_instructions.json @@ -0,0 +1,165 @@ +{ + "version": "", + "name": "", + "instructions": [ + { + "name": "Create", + "accounts": [ + { + "name": "from", + "isMut": true, + "isSigner": true, + "docs": [ + "Payer of the transaction" + ] + }, + { + "name": "to", + "isMut": true, + "isSigner": false, + "docs": [ + "The deterministically defined 'state' account being created via `create_account_with_seed`" + ] + }, + { + "name": "base", + "isMut": false, + "isSigner": false, + "docs": [ + "The program-derived-address signing off on the account creation. Seeds = &[] + bump seed." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The system program" + ] + }, + { + "name": "program", + "isMut": false, + "isSigner": false, + "docs": [ + "The program whose state is being constructed" + ] + } + ], + "args": [ + { + "name": "dataLen", + "type": "u64" + } + ], + "discriminant": { + "type": "u8", + "value": 0 + } + }, + { + "name": "CreateBuffer", + "accounts": [ + { + "name": "buffer", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [], + "discriminant": { + "type": "u8", + "value": 1 + } + }, + { + "name": "SetBuffer", + "accounts": [ + { + "name": "buffer", + "isMut": true, + "isSigner": false, + "docs": [ + "The buffer with the new idl data." + ] + }, + { + "name": "idl", + "isMut": true, + "isSigner": false, + "docs": [ + "The idl account to be updated with the buffer's data." + ] + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [], + "discriminant": { + "type": "u8", + "value": 2 + } + }, + { + "name": "SetAuthority", + "accounts": [ + { + "name": "idl", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "newAuthority", + "type": "publicKey" + } + ], + "discriminant": { + "type": "u8", + "value": 3 + } + }, + { + "name": "Write", + "accounts": [ + { + "name": "idl", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "idlData", + "type": "bytes" + } + ], + "discriminant": { + "type": "u8", + "value": 4 + } + } + ], + "metadata": { + "origin": "shank" + } +} \ No newline at end of file diff --git a/shank-idl/tests/fixtures/instructions/single_file/create_idl_instructions.rs b/shank-idl/tests/fixtures/instructions/single_file/create_idl_instructions.rs new file mode 100644 index 0000000..96da3e8 --- /dev/null +++ b/shank-idl/tests/fixtures/instructions/single_file/create_idl_instructions.rs @@ -0,0 +1,13 @@ +#[derive(ShankInstruction)] +pub enum Instruction { + #[idl_instruction(Create)] + Create, + #[idl_instruction(CreateBuffer)] + CreateBuffer, + #[idl_instruction(SetBuffer)] + SetBuffer, + #[idl_instruction(SetAuthority)] + SetAuthority, + #[idl_instruction(Write)] + Write, +} diff --git a/shank-idl/tests/instructions.rs b/shank-idl/tests/instructions.rs index db720f9..bebe110 100644 --- a/shank-idl/tests/instructions.rs +++ b/shank-idl/tests/instructions.rs @@ -77,6 +77,27 @@ fn instruction_from_single_file_with_multiple_args() { assert_eq!(idl, expected_idl); } +#[test] +fn instruction_from_single_file_with_idl_instructions() { + let file = fixtures_dir() + .join("single_file") + .join("create_idl_instructions.rs"); + let idl = parse_file(file, &ParseIdlConfig::optional_program_address()) + .expect("Parsing should not fail") + .expect("File contains IDL"); + + let expected_idl: Idl = serde_json::from_str(include_str!( + "./fixtures/instructions/single_file/create_idl_instructions.json" + )) + .unwrap(); + + println!("IDL: {}", idl.try_into_json().unwrap()); + + println!("Expected: {}", expected_idl.try_into_json().unwrap()); + + assert_eq!(idl, expected_idl); +} + #[test] fn instruction_from_single_file_with_optional_account() { let file = fixtures_dir() diff --git a/shank-macro-impl/src/instruction/account_attrs.rs b/shank-macro-impl/src/instruction/account_attrs.rs index 33420d9..9a01a9b 100644 --- a/shank-macro-impl/src/instruction/account_attrs.rs +++ b/shank-macro-impl/src/instruction/account_attrs.rs @@ -2,8 +2,8 @@ use std::convert::TryFrom; use proc_macro2::Span; use syn::{ - punctuated::Punctuated, Attribute, Error as ParseError, Ident, Lit, Meta, - MetaList, MetaNameValue, NestedMeta, Result as ParseResult, Token, + punctuated::Punctuated, Attribute, Error as ParseError, Ident, Lit, Meta, MetaList, + MetaNameValue, NestedMeta, Result as ParseResult, Token, }; const IX_ACCOUNT: &str = "account"; @@ -35,9 +35,7 @@ impl InstructionAccount { } } - pub fn from_account_attr( - attr: &Attribute, - ) -> ParseResult { + pub fn from_account_attr(attr: &Attribute) -> ParseResult { let meta = &attr.parse_meta()?; match meta { @@ -75,9 +73,7 @@ impl InstructionAccount { let mut optional = false; for meta in nested { - if let Some((ident, name, value)) = - string_assign_from_nested_meta(meta)? - { + if let Some((ident, name, value)) = string_assign_from_nested_meta(meta)? { // name/desc match name.as_str() { "desc" | "description" | "docs" => desc = Some(value), @@ -88,14 +84,14 @@ impl InstructionAccount { )) } "name" => account_name = Some(value), - _ => return Err(ParseError::new_spanned( - ident, - "Only desc/description or name can be assigned strings", - )), + _ => { + return Err(ParseError::new_spanned( + ident, + "Only desc/description or name can be assigned strings", + )) + } }; - } else if let Some((ident, name)) = - identifier_from_nested_meta(meta) - { + } else if let Some((ident, name)) = identifier_from_nested_meta(meta) { // signer, writable, optional ... match name.as_str() { "signer" | "sign" | "sig" | "s" => signer = true, @@ -143,9 +139,7 @@ impl InstructionAccount { desc, optional, }), - None => { - Err(ParseError::new_spanned(nested, "Missing account name")) - } + None => Err(ParseError::new_spanned(nested, "Missing account name")), } } } @@ -186,14 +180,15 @@ fn string_assign_from_nested_meta( nested_meta: &NestedMeta, ) -> ParseResult> { match nested_meta { - NestedMeta::Meta(Meta::NameValue(MetaNameValue { - path, lit, .. - })) => { + NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. })) => { let ident = path.get_ident(); if let Some(ident) = ident { - let token = match lit { + let token = match lit { Lit::Str(lit) => Ok(lit.value()), - _ => Err(ParseError::new_spanned(ident, "#[account(desc)] arg needs to be assigning to a string")), + _ => Err(ParseError::new_spanned( + ident, + "#[account(desc)] arg needs to be assigning to a string", + )), }?; Ok(Some((ident.clone(), ident.to_string(), token))) } else { @@ -204,14 +199,10 @@ fn string_assign_from_nested_meta( } } -fn identifier_from_nested_meta( - nested_meta: &NestedMeta, -) -> Option<(Ident, String)> { +pub fn identifier_from_nested_meta(nested_meta: &NestedMeta) -> Option<(Ident, String)> { match nested_meta { NestedMeta::Meta(meta) => match meta { - Meta::Path(_) => { - meta.path().get_ident().map(|x| (x.clone(), x.to_string())) - } + Meta::Path(_) => meta.path().get_ident().map(|x| (x.clone(), x.to_string())), // ignore named values and lists _ => None, }, diff --git a/shank-macro-impl/src/instruction/idl_instruction_attrs.rs b/shank-macro-impl/src/instruction/idl_instruction_attrs.rs new file mode 100644 index 0000000..2c2c009 --- /dev/null +++ b/shank-macro-impl/src/instruction/idl_instruction_attrs.rs @@ -0,0 +1,307 @@ +use std::convert::TryFrom; +use std::fmt; + +use proc_macro2::Span; +use syn::{ + punctuated::Punctuated, Attribute, Error as ParseError, Ident, Meta, MetaList, NestedMeta, + Result as ParseResult, Token, +}; + +use crate::{instruction::account_attrs::identifier_from_nested_meta, types::{RustType, RustTypeContext, Primitive}}; +use crate::types::{TypeKind, Composite, Value}; + +use super::{InstructionAccount, InstructionAccounts, InstructionVariantFields}; + +const IX_IDL: &str = "idl_instruction"; + +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum IdlInstruction { + Create, + CreateBuffer, + Write, + SetAuthority, + SetBuffer, +} + +impl IdlInstruction { + fn is_idl_instruction_attr(attr: &Attribute) -> Option<&Attribute> { + match attr.path.get_ident().map(|x| { + x.to_string().as_str() == IX_IDL + }) { + Some(true) => Some(attr), + _ => None, + } + } + + fn from_idl_instruction_attr(attr: &Attribute) -> ParseResult { + let meta = &attr.parse_meta()?; + match meta { + Meta::List(MetaList { nested, .. }) => { + let ident = attr.path.get_ident().map_or_else( + || Ident::new("attr_ident", Span::call_site()), + |x| x.clone(), + ); + Self::parse_idl_instruction_attr_args(ident, nested) + } + Meta::Path(_) | Meta::NameValue(_) => Err(ParseError::new_spanned( + attr, + "#[idl_instruction] attr requires list of arguments", + )), + } + } + + fn parse_idl_instruction_attr_args( + _ident: Ident, + nested: &Punctuated, + ) -> ParseResult { + if nested.is_empty() { + return Err(ParseError::new_spanned( + nested, + "#[idl_instruction] attr requires at least the idl instruction name", + )); + } + if nested.len() > 1 { + return Err(ParseError::new_spanned( + nested, + "#[idl_instruction] attr can only have one idl instruction name", + )); + } + let nested_meta = nested.first().unwrap(); + if let Some((ident, name)) = identifier_from_nested_meta(nested_meta) { + match name.as_str() { + "Create" => Ok(IdlInstruction::Create), + "CreateBuffer" => Ok(IdlInstruction::CreateBuffer), + "SetAuthority" => Ok(IdlInstruction::SetAuthority), + "SetBuffer" => Ok(IdlInstruction::SetBuffer), + "Write" => Ok(IdlInstruction::Write), + _ => Err(ParseError::new_spanned( + ident, + "Invalid/unknown idl instruction name", + )), + } + } else { + Err(ParseError::new_spanned( + nested, + "#[idl_instruction] attr can only have one idl instruction name", + )) + } + } + + pub fn to_accounts(&self, ident: Ident) -> InstructionAccounts { + match self { + IdlInstruction::Create => InstructionAccounts(vec![InstructionAccount { + ident: ident.clone(), + index: Some(0), + name: "from".to_string(), + desc: Some("Payer of the transaction".to_string()), + signer: true, + optional_signer: false, + writable: true, + optional: false, + }, InstructionAccount { + ident: ident.clone(), + index: Some(1), + name: "to".to_string(), + desc: Some("The deterministically defined 'state' account being created via `create_account_with_seed`".to_string()), + signer: false, + optional_signer: false, + writable: true, + optional: false, + }, InstructionAccount { + ident: ident.clone(), + index: Some(2), + name: "base".to_string(), + desc: Some("The program-derived-address signing off on the account creation. Seeds = &[] + bump seed.".to_string()), + signer: false, + optional_signer: false, + writable: false, + optional: false, + }, InstructionAccount { + ident: ident.clone(), + index: Some(3), + name: "system_program".to_string(), + desc: Some("The system program".to_string()), + signer: false, + optional_signer: false, + writable: false, + optional: false, + }, InstructionAccount { + ident, + index: Some(4), + name: "program".to_string(), + desc: Some("The program whose state is being constructed".to_string()), + signer: false, + optional_signer: false, + writable: false, + optional: false, + }]), + IdlInstruction::CreateBuffer => + InstructionAccounts(vec![InstructionAccount { + ident: ident.clone(), + index: Some(0), + name: "buffer".to_string(), + desc: None, + signer: false, + optional_signer: false, + writable: true, + optional: false, + }, InstructionAccount { + ident, + index: Some(1), + name: "authority".to_string(), + desc: None, + signer: true, + optional_signer: false, + writable: false, + optional: false, + }]), + IdlInstruction::SetBuffer => + InstructionAccounts(vec![InstructionAccount { + ident: ident.clone(), + index: Some(0), + name: "buffer".to_string(), + desc: Some("The buffer with the new idl data.".to_string()), + signer: false, + optional_signer: false, + writable: true, + optional: false, + }, InstructionAccount { + ident: ident.clone(), + index: Some(1), + name: "idl".to_string(), + desc: Some("The idl account to be updated with the buffer's data.".to_string()), + signer: false, + optional_signer: false, + writable: true, + optional: false, + }, InstructionAccount { + ident, + index: Some(2), + name: "authority".to_string(), + desc: None, + signer: true, + optional_signer: false, + writable: false, + optional: false, + }]), + IdlInstruction::SetAuthority | IdlInstruction::Write => + InstructionAccounts(vec![InstructionAccount { + ident: ident.clone(), + index: Some(0), + name: "idl".to_string(), + desc: None, + signer: false, + optional_signer: false, + writable: true, + optional: false, + }, InstructionAccount { + ident, + index: Some(2), + name: "authority".to_string(), + desc: None, + signer: true, + optional_signer: false, + writable: false, + optional: false, + }]), + } + } + + pub fn to_instruction_fields(&self, ident: Ident) -> InstructionVariantFields { + match self { + IdlInstruction::Create => InstructionVariantFields::Named( + vec![( + "data_len".to_string(), + RustType { + ident, + kind: TypeKind::Primitive(Primitive::U64), + context: RustTypeContext::Default, + reference: crate::types::ParsedReference::Owned, + } + )] + ), + IdlInstruction::SetAuthority => InstructionVariantFields::Named( + vec![( + "new_authority".to_string(), + RustType { + ident, + kind: TypeKind::Value(Value::Custom("Pubkey".to_string())), + context: RustTypeContext::Default, + reference: crate::types::ParsedReference::Owned + } + )] + ), + IdlInstruction::Write => InstructionVariantFields::Named( + vec![( + "idl_data".to_string(), + RustType { + ident: ident.clone(), + kind: TypeKind::Composite(Composite::Vec, vec![ + RustType { + ident, + kind: TypeKind::Primitive(Primitive::U8), + context: RustTypeContext::CollectionItem, + reference: crate::types::ParsedReference::Owned + } + ]), + context: RustTypeContext::Default, + reference: crate::types::ParsedReference::Owned, + } + )] + ), + IdlInstruction::CreateBuffer | IdlInstruction::SetBuffer => { + InstructionVariantFields::Unnamed(vec![]) + } + } + } +} + +#[derive(Debug, Clone)] +pub enum IdlInstructionError { + TooManyIdlInstructions(ParseError), + NotEnoughIdlInstructions, + OtherErr(syn::Error), +} + +impl fmt::Display for IdlInstructionError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + IdlInstructionError::TooManyIdlInstructions(err) => { + write!(f, "{}", err) + } + IdlInstructionError::NotEnoughIdlInstructions => { + write!(f, "no #[idl_instruction] attributes found") + } + IdlInstructionError::OtherErr(err) => { + write!(f, "{}", err) + } + } + } +} + +impl TryFrom<&[Attribute]> for IdlInstruction { + type Error = IdlInstructionError; + + fn try_from(attrs: &[Attribute]) -> Result { + let idl_instructions = attrs + .iter() + .filter_map(IdlInstruction::is_idl_instruction_attr) + .map(IdlInstruction::from_idl_instruction_attr) + .collect::>>() + .map_err(IdlInstructionError::OtherErr)?; + + if idl_instructions.len() > 1 { + Err(IdlInstructionError::TooManyIdlInstructions( + ParseError::new_spanned( + attrs.get(0), + "Only one #[idl_instruction] attr is allowed per instruction", + ), + )) + } else if idl_instructions.is_empty() { + Err(IdlInstructionError::NotEnoughIdlInstructions) + } else { + let ix = *idl_instructions.get(0).unwrap(); + Ok(ix) + } + } +} diff --git a/shank-macro-impl/src/instruction/instruction.rs b/shank-macro-impl/src/instruction/instruction.rs index 2de5315..7e1141f 100644 --- a/shank-macro-impl/src/instruction/instruction.rs +++ b/shank-macro-impl/src/instruction/instruction.rs @@ -15,7 +15,7 @@ use crate::{ use super::{ account_attrs::{InstructionAccount, InstructionAccounts}, - InstructionStrategies, InstructionStrategy, + IdlInstruction, InstructionStrategies, InstructionStrategy, }; // ----------------- @@ -106,7 +106,7 @@ impl TryFrom<&ParsedEnumVariant> for InstructionVariant { .. } = variant; - let field_tys: InstructionVariantFields = if !fields.is_empty() { + let mut field_tys: InstructionVariantFields = if !fields.is_empty() { // Determine if the InstructionType is tuple or struct variant let field = fields.get(0).unwrap(); match &field.ident { @@ -130,8 +130,24 @@ impl TryFrom<&ParsedEnumVariant> for InstructionVariant { }; let attrs: &[Attribute] = attrs.as_ref(); - let accounts: InstructionAccounts = attrs.try_into()?; - let strategies: InstructionStrategies = attrs.into(); + let accounts: InstructionAccounts; + let strategies: InstructionStrategies; + + let idl_instruction = IdlInstruction::try_from(attrs); + match idl_instruction { + Ok(idl_ix) => { + accounts = idl_ix.to_accounts(ident.clone()); + field_tys = idl_ix.to_instruction_fields(ident.clone()); + strategies = InstructionStrategies(HashSet::< + InstructionStrategy, + >::new()); + } + Err(err) => { + println!("{}", err); + accounts = attrs.try_into()?; + strategies = attrs.into(); + } + } Ok(Self { ident: ident.clone(), diff --git a/shank-macro-impl/src/instruction/mod.rs b/shank-macro-impl/src/instruction/mod.rs index ebc9a91..964a193 100644 --- a/shank-macro-impl/src/instruction/mod.rs +++ b/shank-macro-impl/src/instruction/mod.rs @@ -1,11 +1,13 @@ mod account_attrs; mod extract_instructions; +mod idl_instruction_attrs; #[allow(clippy::module_inception)] mod instruction; mod strategy_attrs; pub use account_attrs::*; pub use extract_instructions::*; +pub use idl_instruction_attrs::*; pub use instruction::*; pub use strategy_attrs::*; diff --git a/shank-macro-impl/src/types/resolve_rust_ty.rs b/shank-macro-impl/src/types/resolve_rust_ty.rs index 86aa9d3..b3b54f3 100644 --- a/shank-macro-impl/src/types/resolve_rust_ty.rs +++ b/shank-macro-impl/src/types/resolve_rust_ty.rs @@ -392,6 +392,7 @@ pub fn resolve_rust_ty( } }; + println!("[resolve_rust_ty] {:?}: {:?}", ident, kind); Ok(RustType { ident, kind,