Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions EXAMPLE_USAGE.md
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];

Comment on lines +10 to +12
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Program ID type should reflect Solana’s Pubkey/declare_id!

Using [u8; 32] for ID can mislead users. Prefer declare_id! so examples align with real projects.

Apply:

-// Program ID - normally created by declare_id! macro
-pub const ID: [u8; 32] = [1; 32];
+// Program ID - typically created by `declare_id!`, which defines `crate::ID: Pubkey`
+use solana_program::{declare_id, pubkey::Pubkey};
+declare_id!("11111111111111111111111111111111");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Program ID - normally created by declare_id! macro
pub const ID: [u8; 32] = [1; 32];
// Program ID - typically created by `declare_id!`, which defines `crate::ID: Pubkey`
use solana_program::{declare_id, pubkey::Pubkey};
declare_id!("11111111111111111111111111111111");
🤖 Prompt for AI Agents
In EXAMPLE_USAGE.md around lines 10 to 12, replace the raw byte array pub const
ID: [u8; 32] = [1; 32]; with the standard Solana pattern using the declare_id!
macro so examples match real projects: import or reference
solana_program::declare_id! (or the appropriate crate path in the example) and
use declare_id!("ReplaceWithYourProgramIdHere"); ensuring the example shows the
macro usage rather than a [u8; 32] literal and updating any surrounding text to
mention inserting the real program id string.

#[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
Copy link

Choose a reason for hiding this comment

The 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 crate::ID; otherwise readers may try to pass a Pubkey.

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];

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In EXAMPLE_USAGE.md around lines 31 to 41, the comment about passing a "program
ID placeholder" is unclear; update the example text to explicitly state that the
placeholder must be an AccountInfo whose key equals crate::ID (not a Pubkey),
and instruct readers to construct or pass an AccountInfo with key == crate::ID
when they want to omit the optional account so the context check behaves as
shown.


## 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

Inconsistent API description: context() with or without program_id

Elsewhere (examples/… and docs), context(program_id, accounts) appears. Here it claims no program_id is needed. Please unify and document the canonical signature and when each form applies.

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" || true

Length 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:

  • Implementations / generator: shank-macro-impl/src/accounts/mod.rs, shank-render/src/context/render_context.rs
  • Macro docs: shank-macro/src/lib.rs
  • Examples / docs: EXAMPLE_USAGE.md, shank/examples/full_solana_example.rs, shank/examples/context_usage_example.rs, shank/examples/anchor_style_example.rs, shank/examples/accounts_struct_example.rs
  • Tests: shank/tests/* (e.g. context_usage_test.rs, final_context_test.rs, context_issue_demo.rs)

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.
Copy link

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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.
🤖 Prompt for AI Agents
In EXAMPLE_USAGE.md around line 64, the sentence asserts "Solana’s modern
optional accounts pattern" as a protocol rule; rephrase to scope it as a
framework/convention instead of a protocol requirement. Update the wording to
indicate this is a common framework or developer convention (e.g., "a common
convention in many Solana frameworks") and avoid language that implies it is
mandated by the Solana protocol.

130 changes: 130 additions & 0 deletions shank-idl/ACCOUNTS_STRUCT_IDL.md
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
Copy link

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[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>,
}
#[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(desc = "System program")]
pub system_program: &'info AccountInfo<'info>,
}
🤖 Prompt for AI Agents
In shank-idl/ACCOUNTS_STRUCT_IDL.md around lines 16 to 24, the account fields
are declared as owned AccountInfo<'info> which is inconsistent with other
examples; update the declarations to use references by changing each field to
&'info AccountInfo<'info> (preserving attributes like mut and signer where
present) so the snippet matches the rest of the codebase and avoids confusion.


#[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
Copy link

Choose a reason for hiding this comment

The 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
In shank-idl/ACCOUNTS_STRUCT_IDL.md around lines 61 to 89, the example JSON for
the CreateVault instruction omits the IdlAccount fields is_optional and
is_optional_signer; update each account object to include both boolean fields
(e.g., "is_optional": false, "is_optional_signer": false) as appropriate for
payer, vault and system_program so consumers see the expected schema shape and
defaults, and update any account docs/comments if a field should be true for
optional cases.


## 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.
Copy link

Choose a reason for hiding this comment

The 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
In shank-idl/ACCOUNTS_STRUCT_IDL.md around line 130, the file is missing a
single trailing newline required by MD047; add exactly one newline character at
the end of the file (ensure the file ends with a single blank line/newline, no
extra blank lines) and re-save so linters pass.

149 changes: 149 additions & 0 deletions shank-idl/src/accounts_extraction.rs
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
Copy link

Choose a reason for hiding this comment

The 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
In shank-idl/src/accounts_extraction.rs around lines 11 to 26, the doc comment
and naming refer to "ShankAccounts" but project guidelines use "ShankAccount"
(singular) causing mismatch; update the comment and any exposed identifiers to
match the canonical name: change the docstring to "Extract ShankAccount structs
and their metadata" (or add a brief compatibility note if keeping the plural),
and update project docs/guidelines to reflect the chosen singular/plural
convention so names are consistent across code and docs.


/// 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
Copy link

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// 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.
/// Extract individual accounts from a ShankAccounts struct by parsing its #[account(...)] field attributes
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.
🤖 Prompt for AI Agents
In shank-idl/src/accounts_extraction.rs around lines 28 to 33, the doc comment
incorrectly states that the function extracts accounts by calling
__shank_accounts(); update the comment to accurately describe current behavior:
explain that the function parses the struct fields and reads their
#[account(...)] attributes at parse time (since __shank_accounts() cannot be
executed), and mention it returns a Vec<InstructionAccount> derived from those
attributes. Keep the comment concise and aligned with the implemented parsing
behavior.

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 {
let has_option_type = matches!(&field.ty, Type::Path(type_path)
if type_path.path.segments.last().map_or(false, |seg| seg.ident == "Option"));
🤖 Prompt for AI Agents
In shank-idl/src/accounts_extraction.rs around lines 70 to 76, the current
Option<T> detection inspects the first path segment which misses fully-qualified
paths like std::option::Option; change the logic to inspect the last path
segment instead (e.g., use path.segments.last()) and compare that segment.ident
to "Option", keeping the existing Type::Path check; ensure you handle the Option
being present or not (map/and_then or pattern) so you don’t panic on empty
segments.

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

Replace 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 Attribute::parse_nested_meta for robust #[account(...)] parsing with alias support

  • In shank-idl/src/accounts_extraction.rs, replace the tokens_str.contains(...) logic with attr.parse_nested_meta(|meta| { … }), matching on meta.path variants:
    • writable: "mut" | "writable" | "write" | "writ" | "w"
    • signer: "signer" | "sign" | "sig" | "s"
    • optional: "optional_signer"; and "optional" | "opt" | "option"
  • Parse desc via meta.value()?.parse::<LitStr>()? instead of manual string slicing (requires full feature for parse_nested_meta) (docs.rs)
  • Remove brittle, order- and spacing-dependent string parsing entirely.
🤖 Prompt for AI Agents
In shank-idl/src/accounts_extraction.rs around lines 80 to 107, replace the
brittle tokens_str.contains(...) string parsing with syn's
Attribute::parse_nested_meta to robustly parse #[account(...)] entries: call
attr.parse_nested_meta(|meta| { ... }) (requires syn "full" feature), match on
meta.path to set writable when the path is any of
"mut","writable","write","writ","w", set signer when path is
"signer","sign","sig","s", set optional_signer for "optional_signer" and
optional for "optional","opt","option"; for desc extract the literal via
meta.value()?.parse::<LitStr>()? instead of manual slicing; remove the old
string-based checks and ensure error propagation or sensible defaults on parse
failures.

}

// 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)
}
7 changes: 6 additions & 1 deletion shank-idl/src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::{
};

use crate::{
accounts_extraction::extract_shank_accounts_structs,
idl::{Idl, IdlConst, IdlEvent, IdlState},
idl_error_code::IdlErrorCode,
idl_instruction::{IdlInstruction, IdlInstructions},
Expand Down Expand Up @@ -107,6 +108,9 @@ fn accounts(ctx: &CrateContext) -> Result<Vec<IdlTypeDefinition>> {
}

fn instructions(ctx: &CrateContext) -> Result<Vec<IdlInstruction>> {
// Extract ShankAccounts structs first
let shank_accounts = extract_shank_accounts_structs(ctx.structs())?;

let instruction_enums =
extract_instruction_enums(ctx.enums()).map_err(parse_error_into)?;

Expand All @@ -116,7 +120,8 @@ fn instructions(ctx: &CrateContext) -> Result<Vec<IdlInstruction>> {
// TODO(thlorenz): Better way to combine those if we don't do the above.

for ix in instruction_enums {
let idl_instructions: IdlInstructions = ix.try_into()?;
// Pass the ShankAccounts information to the IDL conversion
let idl_instructions: IdlInstructions = IdlInstructions::try_into_with_accounts(ix, &shank_accounts)?;
for ix in idl_instructions.0 {
instructions.push(ix);
}
Expand Down
Loading
Loading