-
Couldn't load subscription status.
- Fork 40
Adding ShankAccounts Macro #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,64 @@ | ||||||
| # ShankAccounts with Optional Account Support | ||||||
|
|
||||||
| This demonstrates the improved ShankAccounts implementation with proper optional account handling. | ||||||
|
|
||||||
| ## Example Program Structure | ||||||
|
|
||||||
| ```rust | ||||||
| use shank::ShankAccounts; | ||||||
|
|
||||||
| // Program ID - normally created by declare_id! macro | ||||||
| pub const ID: [u8; 32] = [1; 32]; | ||||||
|
|
||||||
| #[derive(ShankAccounts)] | ||||||
| pub struct CreateVaultAccounts<'info> { | ||||||
| #[account(mut, signer, desc = "The payer and authority")] | ||||||
| pub payer: &'info AccountInfo<'info>, | ||||||
|
|
||||||
| #[account(mut, desc = "The vault account to create")] | ||||||
| pub vault: &'info AccountInfo<'info>, | ||||||
|
|
||||||
| #[account(optional, desc = "Optional new authority")] | ||||||
| pub optional_authority: Option<&'info AccountInfo<'info>>, | ||||||
|
|
||||||
| #[account(desc = "System program")] | ||||||
| pub system_program: &'info AccountInfo<'info>, | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ## Usage | ||||||
|
|
||||||
| ```rust | ||||||
| // When optional account is provided: | ||||||
| let accounts = [payer, vault, authority_account, system_program]; | ||||||
| let ctx = CreateVaultAccounts::context(&accounts); | ||||||
| assert!(ctx.accounts.optional_authority.is_some()); | ||||||
|
|
||||||
| // When optional account is NOT provided (pass program ID as placeholder): | ||||||
| let accounts = [payer, vault, program_id_placeholder, system_program]; | ||||||
| let ctx = CreateVaultAccounts::context(&accounts); | ||||||
| assert!(ctx.accounts.optional_authority.is_none()); | ||||||
| ``` | ||||||
|
Comment on lines
+31
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick Clarify the “program ID placeholder” Explicitly note that the placeholder must be an AccountInfo whose key equals Apply: -// When optional account is NOT provided (pass program ID as placeholder):
-let accounts = [payer, vault, program_id_placeholder, system_program];
+// When optional account is NOT provided (pass an AccountInfo whose key == crate::ID as placeholder):
+// e.g., let program_id_placeholder = AccountInfo { key: &ID.to_bytes(), data: &[], owner: &ID.to_bytes() /* mock */ };
+let accounts = [payer, vault, program_id_placeholder /* AccountInfo with key == crate::ID */, system_program];
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| ## Key Features | ||||||
|
|
||||||
| 1. **Ergonomic API**: No need to pass program ID parameter - uses `crate::ID` automatically | ||||||
| 2. **Type Safety**: Optional accounts use `Option<&AccountInfo>` types | ||||||
| 3. **Runtime Detection**: Checks if `account.key == crate::ID` to determine None/Some | ||||||
| 4. **IDL Generation**: Proper `"isOptional": true` flags in generated IDL | ||||||
| 5. **Remaining Accounts**: Automatically handles extra accounts beyond the struct definition | ||||||
|
Comment on lines
+45
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 🧩 Analysis chainInconsistent API description: context() with or without program_id Elsewhere (examples/… and docs), Would you confirm the intended API and update all docs accordingly? 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "PWD: $(pwd)"
echo "--- repo root listing ---"
ls -la
echo
echo "---- EXAMPLE_USAGE.md (first 200 lines) ----"
if [ -f EXAMPLE_USAGE.md ]; then
sed -n '1,200p' EXAMPLE_USAGE.md || true
else
echo "EXAMPLE_USAGE.md not found at repo root"
fi
echo
echo "---- rg: search for 'context(' across repo ----"
rg -n -S -C2 "context\(" || true
echo
echo "---- rg: search for 'context(program_id' ----"
rg -n -S -C2 "context\(.*program_id" || true
echo
echo "---- rg: search for the exact substring 'context(program_id, accounts' ----"
rg -n -F "context(program_id, accounts" || true
echo
echo "---- rg: look for function/definition 'fn context' / 'def context' / 'function context' ----"
rg -n -S -C2 "fn context\b" || true
rg -n -S -C2 "\bdef context\b" || true
rg -n -S -C2 "\bfunction context\b" || trueLength of output: 28416 Unify the context(...) API to a single canonical signature and apply it across the repo. Repo mixes context(&[AccountInfo]) and context(program_id, accounts) in docs, examples, tests, and generated code — this must be unified. Files to update:
Action required: choose the canonical signature (either accounts-only — using crate::ID as the placeholder for missing optional accounts — or explicit program_id + accounts), update codegen/macros, fix all examples/tests/docs to match, and document the chosen behavior for optional accounts in EXAMPLE_USAGE.md and macro docs. |
||||||
|
|
||||||
| ## IDL Output | ||||||
|
|
||||||
| ```json | ||||||
| { | ||||||
| "accounts": [ | ||||||
| {"name": "payer", "isMut": true, "isSigner": true, "docs": ["The payer and authority"]}, | ||||||
| {"name": "vault", "isMut": true, "isSigner": false, "docs": ["The vault account to create"]}, | ||||||
| {"name": "optionalAuthority", "isMut": false, "isSigner": false, "isOptional": true, "docs": ["Optional new authority"]}, | ||||||
| {"name": "systemProgram", "isMut": false, "isSigner": false, "docs": ["System program"]} | ||||||
| ] | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| This follows Solana's modern optional accounts pattern where missing optional accounts are represented by the program ID. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick Scope the claim about optional-accounts convention “Solana’s modern optional accounts pattern” is framework-convention, not a protocol rule. Rephrase to avoid overstating. Apply: -This follows Solana's modern optional accounts pattern where missing optional accounts are represented by the program ID.
+This follows a common framework convention where missing optional accounts are represented by the program ID.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,130 @@ | ||||||||||||||||||||||||||||||||||||||
| # ShankAccounts IDL Generation | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| This document explains the current state of IDL generation for the new `ShankAccounts` macro and outlines the expected future improvements. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## Current Behavior | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| As of the initial implementation, the `ShankAccounts` macro generates placeholder entries in the IDL instead of expanding the individual accounts from the struct. This is due to the TODO item in `shank-idl/src/idl_instruction.rs` at line 58. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ### Current IDL Output | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| When using `#[accounts(StructName)]` with a `ShankAccounts` struct: | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```rust | ||||||||||||||||||||||||||||||||||||||
| #[derive(ShankAccounts)] | ||||||||||||||||||||||||||||||||||||||
| pub struct CreateVaultAccounts<'info> { | ||||||||||||||||||||||||||||||||||||||
| #[account(mut, signer, desc = "The payer and authority")] | ||||||||||||||||||||||||||||||||||||||
| pub payer: AccountInfo<'info>, | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| #[account(mut, desc = "The vault account to create")] | ||||||||||||||||||||||||||||||||||||||
| pub vault: AccountInfo<'info>, | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| #[account(desc = "System program")] | ||||||||||||||||||||||||||||||||||||||
| pub system_program: AccountInfo<'info>, | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+16
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick Use &'info AccountInfo<'info> in examples for consistency Examples elsewhere (tests/examples) use references. Update this snippet to avoid confusion. - pub payer: AccountInfo<'info>,
+ pub payer: &'info AccountInfo<'info>,
- pub vault: AccountInfo<'info>,
+ pub vault: &'info AccountInfo<'info>,
- pub system_program: AccountInfo<'info>,
+ pub system_program: &'info AccountInfo<'info>,📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| #[derive(ShankInstruction)] | ||||||||||||||||||||||||||||||||||||||
| pub enum VaultInstruction { | ||||||||||||||||||||||||||||||||||||||
| #[accounts(CreateVaultAccounts)] | ||||||||||||||||||||||||||||||||||||||
| CreateVault { seed: String }, | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Currently generates: | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```json | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| "instructions": [ | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| "name": "CreateVault", | ||||||||||||||||||||||||||||||||||||||
| "accounts": [ | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| "name": "__accounts_struct_CreateVaultAccounts", | ||||||||||||||||||||||||||||||||||||||
| "isMut": false, | ||||||||||||||||||||||||||||||||||||||
| "isSigner": false, | ||||||||||||||||||||||||||||||||||||||
| "docs": ["Accounts defined by struct: CreateVaultAccounts"] | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## Expected Future Behavior | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| The IDL generation should be enhanced to resolve `AccountsSource::Struct` and expand the individual accounts from the `ShankAccounts` struct. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ### Expected IDL Output | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| The same code should generate: | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```json | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| "instructions": [ | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| "name": "CreateVault", | ||||||||||||||||||||||||||||||||||||||
| "accounts": [ | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| "name": "payer", | ||||||||||||||||||||||||||||||||||||||
| "isMut": true, | ||||||||||||||||||||||||||||||||||||||
| "isSigner": true, | ||||||||||||||||||||||||||||||||||||||
| "docs": ["The payer and authority"] | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| "name": "vault", | ||||||||||||||||||||||||||||||||||||||
| "isMut": true, | ||||||||||||||||||||||||||||||||||||||
| "isSigner": false, | ||||||||||||||||||||||||||||||||||||||
| "docs": ["The vault account to create"] | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| "name": "system_program", | ||||||||||||||||||||||||||||||||||||||
| "isMut": false, | ||||||||||||||||||||||||||||||||||||||
| "isSigner": false, | ||||||||||||||||||||||||||||||||||||||
| "docs": ["System program"] | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+61
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick Consider documenting optional/optional_signer flags in expected JSON The IdlAccount struct includes is_optional and is_optional_signer. Add them to the example to set expectations. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## Implementation Requirements | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| To achieve the expected behavior, the following changes are needed: | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 1. **Resolve AccountsSource::Struct**: The IDL generation logic in `shank-idl/src/idl_instruction.rs` needs to handle `AccountsSource::Struct` by looking up the referenced struct and extracting its account metadata. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 2. **Access ShankAccounts Metadata**: The IDL generator needs to call the `__shank_accounts()` method generated by the `ShankAccounts` macro to get the account metadata. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 3. **Convert Account Metadata**: Transform the account metadata from the `ShankAccounts` format into the IDL `InstructionAccount` format. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## Current Test Coverage | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| The following tests verify the current placeholder behavior: | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| - `instruction_from_single_file_with_simple_accounts_struct` | ||||||||||||||||||||||||||||||||||||||
| - `instruction_from_single_file_with_accounts_struct` | ||||||||||||||||||||||||||||||||||||||
| - `instruction_from_single_file_with_complex_accounts_struct` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| These tests should be updated when the IDL generation is enhanced to match the expected behavior. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## Benefits of Full Implementation | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Once fully implemented, the `ShankAccounts` system will provide: | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 1. **Single Source of Truth**: Account definitions in one place used for both runtime and IDL generation | ||||||||||||||||||||||||||||||||||||||
| 2. **Type Safety**: Compile-time verification of account usage | ||||||||||||||||||||||||||||||||||||||
| 3. **Anchor-like DX**: Familiar developer experience for Anchor users | ||||||||||||||||||||||||||||||||||||||
| 4. **Backward Compatibility**: Existing shank programs continue to work unchanged | ||||||||||||||||||||||||||||||||||||||
| 5. **Complete IDL**: Full account information in generated IDLs for tooling consumption | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## Macro Integration | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| The `ShankAccounts` macro generates the `__shank_accounts()` method that returns account metadata in this format: | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```rust | ||||||||||||||||||||||||||||||||||||||
| Vec<(u32, &'static str, bool, bool, bool, bool, Option<String>)> | ||||||||||||||||||||||||||||||||||||||
| // (index, name, writable, signer, optional_signer, optional, description) | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| This metadata should be consumed by the IDL generator to produce the expanded account list. | ||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick Add trailing newline Satisfy MD047 (single trailing newline). 🧰 Tools🪛 markdownlint-cli2 (0.18.1)130-130: Files should end with a single newline character (MD047, single-trailing-newline) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,149 @@ | ||||||||||||||||||||||||||
| use anyhow::{format_err, Result}; | ||||||||||||||||||||||||||
| use std::collections::HashMap; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| use shank_macro_impl::{ | ||||||||||||||||||||||||||
| instruction::InstructionAccount, | ||||||||||||||||||||||||||
| parsers::get_derive_attr, | ||||||||||||||||||||||||||
| syn::{ItemStruct, Path, Field, Fields, Type}, | ||||||||||||||||||||||||||
| DERIVE_ACCOUNTS_ATTR, | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /// Extract ShankAccounts structs and their metadata | ||||||||||||||||||||||||||
| pub fn extract_shank_accounts_structs<'a>( | ||||||||||||||||||||||||||
| structs: impl Iterator<Item = &'a ItemStruct>, | ||||||||||||||||||||||||||
| ) -> Result<HashMap<String, Vec<InstructionAccount>>> { | ||||||||||||||||||||||||||
| let mut accounts_map = HashMap::new(); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| for struct_item in structs { | ||||||||||||||||||||||||||
| if let Some(_attr) = get_derive_attr(&struct_item.attrs, DERIVE_ACCOUNTS_ATTR) { | ||||||||||||||||||||||||||
| let struct_name = struct_item.ident.to_string(); | ||||||||||||||||||||||||||
| let accounts = extract_accounts_from_struct(struct_item)?; | ||||||||||||||||||||||||||
| accounts_map.insert(struct_name, accounts); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Ok(accounts_map) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+11
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick Naming mismatch with guidelines (ShankAccount vs ShankAccounts). The code targets ShankAccounts; update docs/guidelines to avoid confusion or provide a compatibility note. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /// Extract individual accounts from a ShankAccounts struct by calling its __shank_accounts method | ||||||||||||||||||||||||||
| fn extract_accounts_from_struct(struct_item: &ItemStruct) -> Result<Vec<InstructionAccount>> { | ||||||||||||||||||||||||||
| // This is where we need to get the account metadata. | ||||||||||||||||||||||||||
| // The challenge is that at parse time, we can't execute the __shank_accounts() method. | ||||||||||||||||||||||||||
| // We need to parse the struct fields and their #[account(...)] attributes directly. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
Comment on lines
+28
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick Fix doc comment to reflect actual behavior. This function parses field attributes; it does not call __shank_accounts(). -/// Extract individual accounts from a ShankAccounts struct by calling its __shank_accounts method
+/// Extract individual accounts from a ShankAccounts struct by parsing its #[account(...)] field attributes📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| let struct_name = &struct_item.ident; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Parse the struct fields and extract account information | ||||||||||||||||||||||||||
| let mut accounts = Vec::new(); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if let Fields::Named(fields) = &struct_item.fields { | ||||||||||||||||||||||||||
| for (index, field) in fields.named.iter().enumerate() { | ||||||||||||||||||||||||||
| let _field_name = field.ident.as_ref() | ||||||||||||||||||||||||||
| .ok_or_else(|| format_err!("Field without name in struct {}", struct_name))?; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Parse the #[account(...)] attributes on this field | ||||||||||||||||||||||||||
| let account = parse_account_attributes(field, index)?; | ||||||||||||||||||||||||||
| accounts.push(account); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| return Err(format_err!( | ||||||||||||||||||||||||||
| "ShankAccounts struct {} must have named fields", | ||||||||||||||||||||||||||
| struct_name | ||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Ok(accounts) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /// Parse #[account(...)] attributes from a struct field | ||||||||||||||||||||||||||
| fn parse_account_attributes(field: &Field, index: usize) -> Result<InstructionAccount> { | ||||||||||||||||||||||||||
| let field_name = field.ident.as_ref().unwrap().to_string(); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Initialize default values | ||||||||||||||||||||||||||
| let mut writable = false; | ||||||||||||||||||||||||||
| let mut signer = false; | ||||||||||||||||||||||||||
| let mut optional = false; | ||||||||||||||||||||||||||
| let mut optional_signer = false; | ||||||||||||||||||||||||||
| let mut desc: Option<String> = None; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Check if the field type is Option<&AccountInfo> to detect optional typing | ||||||||||||||||||||||||||
| let has_option_type = if let Type::Path(type_path) = &field.ty { | ||||||||||||||||||||||||||
| if let Some(segment) = type_path.path.segments.first() { | ||||||||||||||||||||||||||
| segment.ident == "Option" | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| false | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
|
Comment on lines
+70
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Option detection is fragile (uses first segment). Fully-qualified types (e.g., std::option::Option) won’t be detected. Use the last path segment. - let has_option_type = if let Type::Path(type_path) = &field.ty {
- if let Some(segment) = type_path.path.segments.first() {
- segment.ident == "Option"
- } else {
- false
- }
- } else {
- false
- };
+ let has_option_type = matches!(&field.ty, Type::Path(type_path)
+ if type_path.path.segments.last().map_or(false, |seg| seg.ident == "Option"));📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| false | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Parse #[account(...)] attributes | ||||||||||||||||||||||||||
| for attr in &field.attrs { | ||||||||||||||||||||||||||
| if attr.path.is_ident("account") { | ||||||||||||||||||||||||||
| // Use a simple string-based parsing approach for now | ||||||||||||||||||||||||||
| // This is a simplified version - in production we'd want more robust parsing | ||||||||||||||||||||||||||
| let tokens_str = attr.tokens.to_string(); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Simple parsing of common attributes | ||||||||||||||||||||||||||
| if tokens_str.contains("mut") || tokens_str.contains("writable") { | ||||||||||||||||||||||||||
| writable = true; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if tokens_str.contains("signer") { | ||||||||||||||||||||||||||
| signer = true; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if tokens_str.contains("optional_signer") { | ||||||||||||||||||||||||||
| optional_signer = true; | ||||||||||||||||||||||||||
| } else if tokens_str.contains("optional") { | ||||||||||||||||||||||||||
| optional = true; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Extract description using simple regex-like approach | ||||||||||||||||||||||||||
| if let Some(desc_start) = tokens_str.find("desc = \"") { | ||||||||||||||||||||||||||
| let desc_content = &tokens_str[desc_start + 8..]; | ||||||||||||||||||||||||||
| if let Some(desc_end) = desc_content.find('"') { | ||||||||||||||||||||||||||
| desc = Some(desc_content[..desc_end].to_string()); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+80
to
+107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 🧩 Analysis chainReplace string-based parsing with syn’s nested meta; support alias attributes. String contains() is brittle (spacing/ordering) and misses aliases (write/writ/w, sign/sig/s, opt/option). Parse tokens robustly and accept aliases to match the macro’s surface. - // Parse #[account(...)] attributes
- for attr in &field.attrs {
- if attr.path.is_ident("account") {
- // Use a simple string-based parsing approach for now
- // This is a simplified version - in production we'd want more robust parsing
- let tokens_str = attr.tokens.to_string();
-
- // Simple parsing of common attributes
- if tokens_str.contains("mut") || tokens_str.contains("writable") {
- writable = true;
- }
- if tokens_str.contains("signer") {
- signer = true;
- }
- if tokens_str.contains("optional_signer") {
- optional_signer = true;
- } else if tokens_str.contains("optional") {
- optional = true;
- }
-
- // Extract description using simple regex-like approach
- if let Some(desc_start) = tokens_str.find("desc = \"") {
- let desc_content = &tokens_str[desc_start + 8..];
- if let Some(desc_end) = desc_content.find('"') {
- desc = Some(desc_content[..desc_end].to_string());
- }
- }
- }
- }
+ // Parse #[account(...)] attributes robustly and support aliases
+ for attr in &field.attrs {
+ #[allow(deprecated)]
+ if attr.path().is_ident("account") {
+ attr.parse_nested_meta(|meta| {
+ let key = meta.path.segments.last().map(|s| s.ident.to_string()).unwrap_or_default();
+ match key.as_str() {
+ // writable variants
+ "mut" | "writable" | "write" | "writ" | "w" => writable = true,
+ // signer variants
+ "signer" | "sign" | "sig" | "s" => signer = true,
+ // optional variants
+ "optional_signer" => optional_signer = true,
+ "optional" | "opt" | "option" => optional = true,
+ // desc = "..."
+ "desc" => {
+ let val: shank_macro_impl::syn::LitStr = meta.value()?.parse()?;
+ desc = Some(val.value());
+ }
+ _ => {}
+ }
+ Ok(())
+ })?;
+ }
+ }🏁 Script executed: #!/bin/bash
# Ensure syn 2.x is in use (parse_nested_meta API)
rg -n --glob '**/Cargo.toml' -C2 'syn\s*='
# Grep for alias attributes in tests to confirm coverage alignment
rg -n --type=rust -C2 '#\[account\((write|writ|w|sign|sig|s|opt|option)\b'Length of output: 4788 Use
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Handle interaction between Option<> types and attribute flags: | ||||||||||||||||||||||||||
| // - If has Option<> type and optional_signer attribute: only set optional_signer = true | ||||||||||||||||||||||||||
| // - If has Option<> type and optional attribute: set optional = true | ||||||||||||||||||||||||||
| // - If has Option<> type but no attribute: default to optional = true | ||||||||||||||||||||||||||
| if has_option_type && !optional && !optional_signer { | ||||||||||||||||||||||||||
| // If Option<> type but no explicit optional/optional_signer attribute, | ||||||||||||||||||||||||||
| // assume it's a regular optional account | ||||||||||||||||||||||||||
| optional = true; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // For optional_signer accounts, ensure the regular optional flag is not set | ||||||||||||||||||||||||||
| // The IDL should use is_optional_signer=true, is_optional=false for these | ||||||||||||||||||||||||||
| if optional_signer { | ||||||||||||||||||||||||||
| optional = false; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Ok(InstructionAccount { | ||||||||||||||||||||||||||
| index: Some(index as u32), | ||||||||||||||||||||||||||
| ident: field.ident.as_ref().unwrap().clone(), | ||||||||||||||||||||||||||
| name: field_name, | ||||||||||||||||||||||||||
| writable, | ||||||||||||||||||||||||||
| signer, | ||||||||||||||||||||||||||
| optional_signer, | ||||||||||||||||||||||||||
| optional, | ||||||||||||||||||||||||||
| desc, | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /// Resolve accounts for a given struct path | ||||||||||||||||||||||||||
| pub fn resolve_accounts_for_struct_path<'a>( | ||||||||||||||||||||||||||
| accounts_map: &'a HashMap<String, Vec<InstructionAccount>>, | ||||||||||||||||||||||||||
| struct_path: &Path, | ||||||||||||||||||||||||||
| ) -> Option<&'a Vec<InstructionAccount>> { | ||||||||||||||||||||||||||
| let struct_name = struct_path | ||||||||||||||||||||||||||
| .segments | ||||||||||||||||||||||||||
| .last() | ||||||||||||||||||||||||||
| .map(|seg| seg.ident.to_string())?; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| accounts_map.get(&struct_name) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Program ID type should reflect Solana’s Pubkey/declare_id!
Using
[u8; 32]forIDcan mislead users. Preferdeclare_id!so examples align with real projects.Apply:
📝 Committable suggestion
🤖 Prompt for AI Agents