Skip to content

Commit c1c9261

Browse files
lang: Add instruction parser to declare_program! (solana-foundation#4118)
1 parent 70b7db9 commit c1c9261

File tree

6 files changed

+418
-83
lines changed

6 files changed

+418
-83
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ The minor version will be incremented upon a breaking change and the patch versi
1313
### Features
1414

1515
- cli: Added a `check_program_id_mismatch` in build time to check if the program ID in the source code matches the program ID in the keypair file ([#4018](https://github.com/solana-foundation/anchor/pull/4018)). This check will be skipped during `anchor test`.
16+
- lang: lang: Add instruction parser to `declare_program!` ([#4118](https://github.com/solana-foundation/anchor/pull/4118)).
1617

1718
### Fixes
1819

lang/attribute/program/src/declare_program/common.rs

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use anchor_lang_idl::types::{
2-
Idl, IdlArrayLen, IdlDefinedFields, IdlField, IdlGenericArg, IdlRepr, IdlSerialization,
3-
IdlType, IdlTypeDef, IdlTypeDefGeneric, IdlTypeDefTy,
2+
Idl, IdlArrayLen, IdlDefinedFields, IdlField, IdlGenericArg, IdlInstructionAccountItem,
3+
IdlInstructionAccounts, IdlRepr, IdlSerialization, IdlType, IdlTypeDef, IdlTypeDefGeneric,
4+
IdlTypeDefTy,
45
};
56
use proc_macro2::Literal;
67
use quote::{format_ident, quote};
@@ -369,3 +370,71 @@ fn handle_defined_fields<R>(
369370
_ => unit_cb(),
370371
}
371372
}
373+
374+
/// Combine regular instruction accounts with non-instruction composite accounts.
375+
pub fn get_all_instruction_accounts(idl: &Idl) -> Vec<IdlInstructionAccounts> {
376+
// It's possible to declare an accounts struct and not use it as an instruction, see
377+
// https://github.com/coral-xyz/anchor/issues/3274
378+
//
379+
// NOTE: Returned accounts will not be unique if non-instruction composite accounts have been
380+
// used multiple times https://github.com/solana-foundation/anchor/issues/3349
381+
fn get_non_instruction_composite_accounts<'a>(
382+
accs: &'a [IdlInstructionAccountItem],
383+
idl: &'a Idl,
384+
) -> Vec<&'a IdlInstructionAccounts> {
385+
accs.iter()
386+
.flat_map(|acc| match acc {
387+
IdlInstructionAccountItem::Composite(accs)
388+
if !idl
389+
.instructions
390+
.iter()
391+
.any(|ix| ix.accounts == accs.accounts) =>
392+
{
393+
let mut nica = get_non_instruction_composite_accounts(&accs.accounts, idl);
394+
nica.push(accs);
395+
nica
396+
}
397+
_ => Default::default(),
398+
})
399+
.collect()
400+
}
401+
402+
let ix_accs = idl
403+
.instructions
404+
.iter()
405+
.flat_map(|ix| ix.accounts.to_owned())
406+
.collect::<Vec<_>>();
407+
get_non_instruction_composite_accounts(&ix_accs, idl)
408+
.into_iter()
409+
.fold(Vec::<IdlInstructionAccounts>::default(), |mut all, accs| {
410+
// Make sure they are unique
411+
if all.iter().all(|a| a.accounts != accs.accounts) {
412+
// The name is not guaranteed to be the same as the one used in the actual source
413+
// code of the program because the IDL only stores the field names
414+
let name = if all.iter().all(|a| a.name != accs.name) {
415+
accs.name.to_owned()
416+
} else {
417+
// Append numbers to the field name until we find a unique name
418+
(2..)
419+
.find_map(|i| {
420+
let name = format!("{}{i}", accs.name);
421+
all.iter().all(|a| a.name != name).then_some(name)
422+
})
423+
.expect("Should always find a valid name")
424+
};
425+
426+
all.push(IdlInstructionAccounts {
427+
name,
428+
accounts: accs.accounts.to_owned(),
429+
})
430+
}
431+
432+
all
433+
})
434+
.into_iter()
435+
.chain(idl.instructions.iter().map(|ix| IdlInstructionAccounts {
436+
name: ix.name.to_owned(),
437+
accounts: ix.accounts.to_owned(),
438+
}))
439+
.collect()
440+
}

lang/attribute/program/src/declare_program/mods/internal.rs

Lines changed: 15 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
use anchor_lang_idl::types::{
2-
Idl, IdlInstruction, IdlInstructionAccountItem, IdlInstructionAccounts,
3-
};
1+
use anchor_lang_idl::types::{Idl, IdlInstructionAccountItem};
42
use anchor_syn::{
53
codegen::accounts::{__client_accounts, __cpi_client_accounts},
64
parser::accounts,
@@ -9,7 +7,10 @@ use anchor_syn::{
97
use heck::CamelCase;
108
use quote::{format_ident, quote};
119

12-
use super::common::{convert_idl_type_to_syn_type, gen_discriminator, get_canonical_program_id};
10+
use super::common::{
11+
convert_idl_type_to_syn_type, gen_discriminator, get_all_instruction_accounts,
12+
get_canonical_program_id,
13+
};
1314

1415
pub fn gen_internal_mod(idl: &Idl) -> proc_macro2::TokenStream {
1516
let internal_args_mod = gen_internal_args_mod(idl);
@@ -107,83 +108,17 @@ fn gen_internal_accounts_common(
107108
idl: &Idl,
108109
gen_accounts: impl Fn(&AccountsStruct, proc_macro2::TokenStream) -> proc_macro2::TokenStream,
109110
) -> proc_macro2::TokenStream {
110-
// It's possible to declare an accounts struct and not use it as an instruction, see
111-
// https://github.com/coral-xyz/anchor/issues/3274
112-
//
113-
// NOTE: Returned accounts will not be unique if non-instruction composite accounts have been
114-
// used multiple times https://github.com/solana-foundation/anchor/issues/3349
115-
fn get_non_instruction_composite_accounts<'a>(
116-
accs: &'a [IdlInstructionAccountItem],
117-
idl: &'a Idl,
118-
) -> Vec<&'a IdlInstructionAccounts> {
119-
accs.iter()
120-
.flat_map(|acc| match acc {
121-
IdlInstructionAccountItem::Composite(accs)
122-
if !idl
123-
.instructions
124-
.iter()
125-
.any(|ix| ix.accounts == accs.accounts) =>
126-
{
127-
let mut nica = get_non_instruction_composite_accounts(&accs.accounts, idl);
128-
nica.push(accs);
129-
nica
130-
}
131-
_ => Default::default(),
132-
})
133-
.collect()
134-
}
135-
136-
// Combine regular instructions with non-instruction composite accounts
137-
let ix_accs = idl
138-
.instructions
139-
.iter()
140-
.flat_map(|ix| ix.accounts.to_owned())
141-
.collect::<Vec<_>>();
142-
let combined_ixs = get_non_instruction_composite_accounts(&ix_accs, idl)
143-
.into_iter()
144-
.fold(Vec::<IdlInstruction>::default(), |mut ixs, accs| {
145-
// Make sure they are unique
146-
if ixs.iter().all(|ix| ix.accounts != accs.accounts) {
147-
// The name is not guaranteed to be the same as the one used in the actual source
148-
// code of the program because the IDL only stores the field names.
149-
let name = if ixs.iter().all(|ix| ix.name != accs.name) {
150-
accs.name.to_owned()
151-
} else {
152-
// Append numbers to the field name until we find a unique name
153-
(2..)
154-
.find_map(|i| {
155-
let name = format!("{}{i}", accs.name);
156-
ixs.iter().all(|ix| ix.name != name).then_some(name)
157-
})
158-
.expect("Should always find a valid name")
159-
};
160-
161-
ixs.push(IdlInstruction {
162-
name,
163-
accounts: accs.accounts.to_owned(),
164-
args: Default::default(),
165-
discriminator: Default::default(),
166-
docs: Default::default(),
167-
returns: Default::default(),
168-
})
169-
}
170-
171-
ixs
172-
})
173-
.into_iter()
174-
.chain(idl.instructions.iter().cloned())
175-
.collect::<Vec<_>>();
176-
177-
let accounts = combined_ixs
111+
let all_ix_accs = get_all_instruction_accounts(idl);
112+
let accounts = all_ix_accs
178113
.iter()
179-
.map(|ix| {
180-
let ident = format_ident!("{}", ix.name.to_camel_case());
181-
let generics = if ix.accounts.is_empty() {
114+
.map(|accs| {
115+
let ident = format_ident!("{}", accs.name.to_camel_case());
116+
let generics = if accs.accounts.is_empty() {
182117
quote!()
183118
} else {
184119
quote!(<'info>)
185120
};
186-
let accounts = ix.accounts.iter().map(|acc| match acc {
121+
let accounts = accs.accounts.iter().map(|acc| match acc {
187122
IdlInstructionAccountItem::Single(acc) => {
188123
let name = format_ident!("{}", acc.name);
189124

@@ -212,11 +147,11 @@ fn gen_internal_accounts_common(
212147
}
213148
IdlInstructionAccountItem::Composite(accs) => {
214149
let name = format_ident!("{}", accs.name);
215-
let ty_name = combined_ixs
150+
let ty_name = all_ix_accs
216151
.iter()
217-
.find(|ix| ix.accounts == accs.accounts)
218-
.map(|ix| format_ident!("{}", ix.name.to_camel_case()))
219-
.expect("Instruction must exist");
152+
.find(|a| a.accounts == accs.accounts)
153+
.map(|a| format_ident!("{}", a.name.to_camel_case()))
154+
.expect("Accounts must exist");
220155

221156
quote! {
222157
pub #name: #ty_name #generics

lang/attribute/program/src/declare_program/mods/utils.rs

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
use anchor_lang_idl::types::Idl;
1+
use anchor_lang_idl::types::{Idl, IdlInstructionAccountItem, IdlInstructionAccounts};
2+
use heck::CamelCase;
23
use quote::{format_ident, quote};
34

5+
use super::common::{get_all_instruction_accounts, get_canonical_program_id};
6+
47
pub fn gen_utils_mod(idl: &Idl) -> proc_macro2::TokenStream {
58
let account = gen_account(idl);
69
let event = gen_event(idl);
10+
let instruction = gen_instruction(idl);
711

812
quote! {
913
/// Program utilities.
@@ -12,6 +16,7 @@ pub fn gen_utils_mod(idl: &Idl) -> proc_macro2::TokenStream {
1216

1317
#account
1418
#event
19+
#instruction
1520
}
1621
}
1722
}
@@ -107,3 +112,119 @@ fn gen_event(idl: &Idl) -> proc_macro2::TokenStream {
107112
}
108113
}
109114
}
115+
116+
fn gen_instruction(idl: &Idl) -> proc_macro2::TokenStream {
117+
let variants = idl
118+
.instructions
119+
.iter()
120+
.map(|ix| format_ident!("{}", ix.name.to_camel_case())).map(
121+
|name| quote! { #name { accounts: client::accounts::#name, args: client::args::#name } },
122+
);
123+
let if_statements = {
124+
fn gen_accounts(
125+
name: &str,
126+
ix_accs: &[IdlInstructionAccountItem],
127+
all_ix_accs: &[IdlInstructionAccounts],
128+
) -> proc_macro2::TokenStream {
129+
let name = format_ident!("{}", name.to_camel_case());
130+
let fields = ix_accs.iter().map(|acc| match acc {
131+
IdlInstructionAccountItem::Single(acc) => {
132+
let name = format_ident!("{}", acc.name);
133+
let signer = acc.signer;
134+
let writable = acc.writable;
135+
quote! {
136+
#name: {
137+
let acc = accs.next().ok_or_else(|| ProgramError::NotEnoughAccountKeys)?;
138+
if acc.is_signer != #signer {
139+
return Err(ProgramError::InvalidAccountData.into());
140+
}
141+
if acc.is_writable != #writable {
142+
return Err(ProgramError::InvalidAccountData.into());
143+
}
144+
145+
acc.pubkey
146+
}
147+
}
148+
}
149+
IdlInstructionAccountItem::Composite(accs) => {
150+
let name = format_ident!("{}", accs.name);
151+
let accounts = all_ix_accs
152+
.iter()
153+
.find(|a| a.accounts == accs.accounts)
154+
.map(|a| gen_accounts(&a.name, &a.accounts, all_ix_accs))
155+
.expect("Accounts must exist");
156+
quote! { #name: #accounts }
157+
}
158+
});
159+
160+
quote! { client::accounts::#name { #(#fields,)* } }
161+
}
162+
163+
let all_ix_accs = get_all_instruction_accounts(idl);
164+
idl.instructions
165+
.iter()
166+
.map(|ix| {
167+
let name = format_ident!("{}", ix.name.to_camel_case());
168+
let accounts = gen_accounts(&ix.name, &ix.accounts, &all_ix_accs);
169+
quote! {
170+
if ix.data.starts_with(client::args::#name::DISCRIMINATOR) {
171+
let mut accs = ix.accounts.to_owned().into_iter();
172+
return Ok(Self::#name {
173+
accounts: #accounts,
174+
args: client::args::#name::try_from_slice(
175+
&ix.data[client::args::#name::DISCRIMINATOR.len()..]
176+
)?
177+
})
178+
}
179+
}
180+
})
181+
.collect::<Vec<_>>()
182+
};
183+
184+
let solana_instruction = quote!(anchor_lang::solana_program::instruction::Instruction);
185+
let program_id = get_canonical_program_id();
186+
187+
quote! {
188+
/// An enum that includes all instructions of the declared program.
189+
///
190+
/// See [`Self::try_from_solana_instruction`] to create an instance from
191+
/// [`anchor_lang::solana_program::instruction::Instruction`].
192+
pub enum Instruction {
193+
#(#variants,)*
194+
}
195+
196+
impl Instruction {
197+
/// Try to create an instruction based on the given
198+
/// [`anchor_lang::solana_program::instruction::Instruction`].
199+
///
200+
/// This method checks:
201+
///
202+
/// - The program ID
203+
/// - There is no missing account(s)
204+
/// - All accounts have the correct signer and writable attributes
205+
/// - The instruction data can be deserialized
206+
///
207+
/// It does **not** check whether:
208+
///
209+
/// - There are more accounts than expected
210+
/// - The account addresses match the ones that could be derived using the resolution
211+
/// fields such as `address` and `pda`
212+
pub fn try_from_solana_instruction(ix: &#solana_instruction) -> Result<Self> {
213+
Self::try_from(ix)
214+
}
215+
}
216+
217+
impl TryFrom<&#solana_instruction> for Instruction {
218+
type Error = anchor_lang::error::Error;
219+
220+
fn try_from(ix: &#solana_instruction) -> Result<Self> {
221+
if ix.program_id != #program_id {
222+
return Err(ProgramError::IncorrectProgramId.into())
223+
}
224+
225+
#(#if_statements)*
226+
Err(ProgramError::InvalidInstructionData.into())
227+
}
228+
}
229+
}
230+
}

0 commit comments

Comments
 (0)