Skip to content
Merged
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
41 changes: 32 additions & 9 deletions lang/attribute/program/src/declare_program/mods/parsers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,17 +133,40 @@ fn gen_instruction(idl: &Idl) -> proc_macro2::TokenStream {
let name = format_ident!("{}", acc.name);
let signer = acc.signer;
let writable = acc.writable;
quote! {
#name: {
let acc = accs.next().ok_or_else(|| ProgramError::NotEnoughAccountKeys)?;
if acc.is_signer != #signer {
return Err(ProgramError::InvalidAccountData.into());
let optional = acc.optional;
if optional {
// For optional accounts, the program ID is used as a placeholder when missing
let program_id = get_canonical_program_id();
quote! {
#name: {
let acc = accs.next().ok_or_else(|| ProgramError::NotEnoughAccountKeys)?;
// Check if this is a placeholder (program_id used for missing optional accounts)
if acc.pubkey == #program_id {
None
} else {
if acc.is_signer != #signer {
return Err(ProgramError::InvalidAccountData.into());
}
if acc.is_writable != #writable {
return Err(ProgramError::InvalidAccountData.into());
}
Some(acc.pubkey)
}
}
if acc.is_writable != #writable {
return Err(ProgramError::InvalidAccountData.into());
}
} else {
quote! {
#name: {
let acc = accs.next().ok_or_else(|| ProgramError::NotEnoughAccountKeys)?;
if acc.is_signer != #signer {
return Err(ProgramError::InvalidAccountData.into());
}
if acc.is_writable != #writable {
return Err(ProgramError::InvalidAccountData.into());
}

acc.pubkey
}

acc.pubkey
}
}
}
Expand Down
42 changes: 42 additions & 0 deletions tests/declare-program/idls/external.json
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,48 @@
"type": "u32"
}
]
},
{
"name": "update_with_optional",
"discriminator": [
119,
204,
186,
47,
151,
121,
221,
126
],
"accounts": [
{
"name": "authority",
"signer": true
},
{
"name": "my_account",
"writable": true,
"pda": {
"seeds": [
{
"kind": "account",
"path": "authority"
}
]
}
},
{
"name": "optional_account",
"writable": true,
"optional": true
}
],
"args": [
{
"name": "value",
"type": "u32"
}
]
}
],
"accounts": [
Expand Down
54 changes: 54 additions & 0 deletions tests/declare-program/programs/declare-program/tests/parsers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,60 @@ pub fn test_instruction_parser() {
val.serialize(&mut w).unwrap();
w
}

// Test optional account parsing
let authority = Pubkey::from_str_const("Authority1111111111111111111111111111111111");
let my_account = Pubkey::from_str_const("MyAccount1111111111111111111111111111111111");
let optional_account = Pubkey::from_str_const("optiona1Acc11111111111111111111111111111111");
let expected_args = external::client::args::UpdateWithOptional { value: 5 };

// Test with optional account present (Some)
match Instruction::parse(&SolanaInstruction::new_with_bytes(
external::ID,
&[
external::client::args::UpdateWithOptional::DISCRIMINATOR,
&ser(&expected_args),
]
.concat(),
vec![
AccountMeta::new_readonly(authority, true),
AccountMeta::new(my_account, false),
AccountMeta::new(optional_account, false),
],
)) {
Ok(Instruction::UpdateWithOptional { accounts, args }) => {
assert_eq!(accounts.authority, authority);
assert_eq!(accounts.my_account, my_account);
assert_eq!(accounts.optional_account, Some(optional_account));
assert_eq!(args.value, expected_args.value);
}
Ok(_) => panic!("Expected UpdateWithOptional instruction variant"),
Err(e) => panic!("Expected Ok result, got error: {:?}", e),
};

// Test with optional account missing (None - using program ID as placeholder)
match Instruction::parse(&SolanaInstruction::new_with_bytes(
external::ID,
&[
external::client::args::UpdateWithOptional::DISCRIMINATOR,
&ser(&expected_args),
]
.concat(),
vec![
AccountMeta::new_readonly(authority, true),
AccountMeta::new(my_account, false),
AccountMeta::new(external::ID, false), // Program ID as placeholder for missing optional
],
)) {
Ok(Instruction::UpdateWithOptional { accounts, args }) => {
assert_eq!(accounts.authority, authority);
assert_eq!(accounts.my_account, my_account);
assert_eq!(accounts.optional_account, None);
assert_eq!(args.value, expected_args.value);
}
Ok(_) => panic!("Expected UpdateWithOptional instruction variant"),
Err(e) => panic!("Expected Ok result, got error: {:?}", e),
};
}

#[test]
Expand Down
16 changes: 16 additions & 0 deletions tests/declare-program/programs/external/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ pub mod external {
pub fn test_compilation_no_accounts(_ctx: Context<TestCompilationNoAccounts>) -> Result<()> {
Ok(())
}

// Test optional accounts parsing
pub fn update_with_optional(ctx: Context<UpdateWithOptional>, value: u32) -> Result<()> {
ctx.accounts.my_account.field = value;
Ok(())
}
}

#[error_code]
Expand Down Expand Up @@ -182,6 +188,16 @@ pub struct NonInstructionUpdate2<'info> {
pub program: Program<'info, program::External>,
}

#[derive(Accounts)]
pub struct UpdateWithOptional<'info> {
pub authority: Signer<'info>,
#[account(mut, seeds = [authority.key.as_ref()], bump)]
pub my_account: Account<'info, MyAccount>,
/// CHECK: Optional account for testing
#[account(mut)]
pub optional_account: Option<AccountInfo<'info>>,
}

#[account]
pub struct MyAccount {
pub field: u32,
Expand Down