Skip to content

Commit 5e338c2

Browse files
committed
Add IDL metadata for SPL mint/token accounts and PodOption
1 parent a1c0ca1 commit 5e338c2

File tree

2 files changed

+311
-3
lines changed

2 files changed

+311
-3
lines changed

star_frame_spl/src/pod.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,26 @@ where
100100
}
101101
}
102102
}
103+
104+
#[cfg(all(feature = "idl", not(target_os = "solana")))]
105+
mod idl_impl {
106+
use super::*;
107+
use star_frame::{
108+
idl::TypeToIdl,
109+
star_frame_idl::{ty::IdlTypeDef, IdlDefinition},
110+
};
111+
112+
impl<T> TypeToIdl for PodOption<T>
113+
where
114+
T: TypeToIdl + Pod + Default,
115+
{
116+
type AssociatedProgram = <T as TypeToIdl>::AssociatedProgram;
117+
118+
fn type_to_idl(idl_definition: &mut IdlDefinition) -> star_frame::IdlResult<IdlTypeDef> {
119+
Ok(IdlTypeDef::Option {
120+
ty: Box::new(T::type_to_idl(idl_definition)?),
121+
fixed: true,
122+
})
123+
}
124+
}
125+
}

star_frame_spl/src/token/state.rs

Lines changed: 288 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use star_frame::{
2323
/// It validates the account data on validate and provides cheap accessor methods for accessing fields
2424
/// without deserializing the entire account data.
2525
#[derive(AccountSet, Debug, Clone)]
26+
#[account_set(skip_default_idl)]
2627
#[validate(extra_validation = self.validate())]
2728
#[validate(
2829
id = "validate_mint", arg = ValidateMint<'a>, generics = [<'a>],
@@ -46,8 +47,19 @@ impl HasInnerType for MintAccount {
4647

4748
/// See [`spl_token_interface::state::Mint`].
4849
#[derive(
49-
Debug, Clone, PartialEq, Eq, Copy, Default, Zeroable, CheckedBitPattern, Align1, NoUninit,
50+
Debug,
51+
Clone,
52+
PartialEq,
53+
Eq,
54+
Copy,
55+
Default,
56+
Zeroable,
57+
CheckedBitPattern,
58+
Align1,
59+
NoUninit,
60+
TypeToIdl,
5061
)]
62+
#[type_to_idl(program = crate::token::Token)]
5163
#[repr(C, packed)]
5264
pub struct MintAccountData {
5365
pub mint_authority: PodOption<Pubkey>,
@@ -267,6 +279,7 @@ where
267279
/// It validates the account data on validate and provides cheap accessor methods for accessing fields
268280
/// without deserializing the entire account data, although it does provide full deserialization methods.
269281
#[derive(AccountSet, Debug, Clone)]
282+
#[account_set(skip_default_idl)]
270283
#[validate(extra_validation = self.validate())]
271284
#[validate(
272285
id = "validate_token",
@@ -292,8 +305,19 @@ impl HasInnerType for TokenAccount {
292305

293306
/// See [`spl_token_interface::state::AccountState`].
294307
#[derive(
295-
Debug, Clone, PartialEq, Eq, Copy, Default, Zeroable, CheckedBitPattern, Align1, NoUninit,
308+
Debug,
309+
Clone,
310+
PartialEq,
311+
Eq,
312+
Copy,
313+
Default,
314+
Zeroable,
315+
CheckedBitPattern,
316+
Align1,
317+
NoUninit,
318+
TypeToIdl,
296319
)]
320+
#[type_to_idl(program = crate::token::Token)]
297321
#[repr(u8)]
298322
pub enum AccountState {
299323
/// Account is not yet initialized
@@ -308,7 +332,10 @@ pub enum AccountState {
308332
}
309333

310334
/// See [`spl_token_interface::state::Account`].
311-
#[derive(Clone, Copy, Debug, Default, PartialEq, CheckedBitPattern, Zeroable, NoUninit)]
335+
#[derive(
336+
Clone, Copy, Debug, Default, PartialEq, CheckedBitPattern, Zeroable, NoUninit, TypeToIdl,
337+
)]
338+
#[type_to_idl(program = crate::token::Token)]
312339
#[repr(C, packed)]
313340
pub struct TokenAccountData {
314341
pub mint: KeyFor<MintAccount>,
@@ -493,9 +520,267 @@ where
493520
}
494521
}
495522

523+
#[cfg(all(feature = "idl", not(target_os = "solana")))]
524+
mod idl_impl {
525+
use super::*;
526+
use star_frame::{
527+
idl::{AccountSetToIdl, AccountToIdl, ProgramToIdl, TypeToIdl},
528+
star_frame_idl::{
529+
account::{IdlAccount, IdlAccountId},
530+
account_set::IdlAccountSetDef,
531+
item_source, IdlDefinition,
532+
},
533+
};
534+
535+
fn register_spl_account<T: TypeToIdl>(
536+
idl_definition: &mut IdlDefinition,
537+
) -> star_frame::IdlResult<IdlAccountId> {
538+
let type_def = T::type_to_idl(idl_definition)?;
539+
let type_id = type_def.assert_defined()?.clone();
540+
let namespace = <T::AssociatedProgram as ProgramToIdl>::crate_metadata().name;
541+
let idl_account = IdlAccount {
542+
discriminant: Vec::new(),
543+
type_id,
544+
seeds: None,
545+
};
546+
let namespace = idl_definition.add_account(idl_account, namespace)?;
547+
Ok(IdlAccountId {
548+
namespace,
549+
source: item_source::<T>(),
550+
})
551+
}
552+
553+
impl AccountToIdl for MintAccountData {
554+
fn account_to_idl(
555+
idl_definition: &mut IdlDefinition,
556+
) -> star_frame::IdlResult<IdlAccountId> {
557+
register_spl_account::<Self>(idl_definition)
558+
}
559+
}
560+
561+
impl AccountToIdl for TokenAccountData {
562+
fn account_to_idl(
563+
idl_definition: &mut IdlDefinition,
564+
) -> star_frame::IdlResult<IdlAccountId> {
565+
register_spl_account::<Self>(idl_definition)
566+
}
567+
}
568+
569+
impl<A> AccountSetToIdl<A> for MintAccount
570+
where
571+
AccountInfo: AccountSetToIdl<A>,
572+
{
573+
fn account_set_to_idl(
574+
idl_definition: &mut IdlDefinition,
575+
arg: A,
576+
) -> star_frame::IdlResult<IdlAccountSetDef> {
577+
let mut set = AccountInfo::account_set_to_idl(idl_definition, arg)?;
578+
set.single()?
579+
.program_accounts
580+
.push(MintAccountData::account_to_idl(idl_definition)?);
581+
Ok(set)
582+
}
583+
}
584+
585+
impl<A> AccountSetToIdl<A> for TokenAccount
586+
where
587+
AccountInfo: AccountSetToIdl<A>,
588+
{
589+
fn account_set_to_idl(
590+
idl_definition: &mut IdlDefinition,
591+
arg: A,
592+
) -> star_frame::IdlResult<IdlAccountSetDef> {
593+
let mut set = AccountInfo::account_set_to_idl(idl_definition, arg)?;
594+
set.single()?
595+
.program_accounts
596+
.push(TokenAccountData::account_to_idl(idl_definition)?);
597+
Ok(set)
598+
}
599+
}
600+
}
601+
496602
#[cfg(test)]
497603
mod tests {
498604
use super::*;
605+
#[cfg(all(feature = "idl", not(target_os = "solana")))]
606+
use star_frame::empty_star_frame_instruction;
607+
608+
#[cfg(all(feature = "idl", not(target_os = "solana")))]
609+
mod mini_program {
610+
use super::*;
611+
use star_frame::star_frame_idl::{CrateMetadata, Version};
612+
613+
#[derive(
614+
Copy, Clone, Debug, Eq, PartialEq, InstructionArgs, BorshDeserialize, BorshSerialize,
615+
)]
616+
#[type_to_idl(program = crate::token::state::tests::mini_program::MintTokenTestProgram)]
617+
pub struct TouchSplAccounts;
618+
619+
#[derive(Debug, Clone, AccountSet)]
620+
pub struct TouchSplAccountsAccounts {
621+
pub mint: MintAccount,
622+
pub token: TokenAccount,
623+
}
624+
empty_star_frame_instruction!(TouchSplAccounts, TouchSplAccountsAccounts);
625+
626+
#[derive(Copy, Debug, Clone, PartialEq, Eq, InstructionSet)]
627+
#[ix_set(use_repr)]
628+
#[repr(u8)]
629+
pub enum MintTokenInstructionSet {
630+
TouchSplAccounts(TouchSplAccounts),
631+
}
632+
633+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
634+
pub struct MintTokenTestProgram;
635+
636+
impl StarFrameProgram for MintTokenTestProgram {
637+
type InstructionSet = MintTokenInstructionSet;
638+
type AccountDiscriminant = ();
639+
const ID: Pubkey = pubkey!("11111111111111111111111111111111");
640+
}
641+
642+
impl ProgramToIdl for MintTokenTestProgram {
643+
type Errors = ();
644+
fn crate_metadata() -> CrateMetadata {
645+
CrateMetadata {
646+
name: "mint_token_test_program".to_string(),
647+
version: Version::new(0, 0, 1),
648+
..Default::default()
649+
}
650+
}
651+
}
652+
}
653+
654+
#[cfg(all(feature = "idl", not(target_os = "solana")))]
655+
#[test]
656+
fn mint_and_token_accounts_emit_idl_entries() -> Result<()> {
657+
use star_frame::{
658+
idl::{AccountSetToIdl, AccountToIdl, ProgramToIdl},
659+
star_frame_idl::{account_set::IdlAccountSetDef, IdlDefinition, IdlMetadata},
660+
};
661+
662+
let mut idl = IdlDefinition {
663+
address: Token::ID,
664+
metadata: IdlMetadata {
665+
crate_metadata: Token::crate_metadata(),
666+
..Default::default()
667+
},
668+
..Default::default()
669+
};
670+
671+
let mint_account_id = MintAccountData::account_to_idl(&mut idl)?;
672+
assert!(
673+
idl.accounts.contains_key(&mint_account_id.source),
674+
"MintAccountData definition missing from IDL"
675+
);
676+
assert!(
677+
idl.types.contains_key(&mint_account_id.source),
678+
"MintAccountData TypeToIdl definition missing"
679+
);
680+
681+
let token_account_id = TokenAccountData::account_to_idl(&mut idl)?;
682+
assert!(
683+
idl.accounts.contains_key(&token_account_id.source),
684+
"TokenAccountData definition missing from IDL"
685+
);
686+
assert!(
687+
idl.types.contains_key(&token_account_id.source),
688+
"TokenAccountData TypeToIdl definition missing"
689+
);
690+
691+
match MintAccount::account_set_to_idl(&mut idl, ())? {
692+
IdlAccountSetDef::Single(single) => {
693+
assert!(
694+
single.program_accounts.contains(&mint_account_id),
695+
"MintAccount did not register its program account entry",
696+
);
697+
}
698+
other => panic!("MintAccount should produce a single account set, got {other:?}"),
699+
}
700+
701+
match TokenAccount::account_set_to_idl(&mut idl, ())? {
702+
IdlAccountSetDef::Single(single) => {
703+
assert!(
704+
single.program_accounts.contains(&token_account_id),
705+
"TokenAccount did not register its program account entry",
706+
);
707+
}
708+
other => panic!("TokenAccount should produce a single account set, got {other:?}"),
709+
}
710+
711+
Ok(())
712+
}
713+
714+
#[cfg(all(feature = "idl", not(target_os = "solana")))]
715+
#[test]
716+
fn mini_program_uses_spl_accounts_in_idl() -> Result<()> {
717+
use mini_program::*;
718+
use star_frame::{
719+
idl::ProgramToIdl,
720+
star_frame_idl::{
721+
account_set::{IdlAccountSetDef, IdlAccountSetStructField},
722+
item_source, IdlDefinition,
723+
},
724+
};
725+
726+
fn struct_fields<'a>(
727+
idl: &'a IdlDefinition,
728+
def: &'a IdlAccountSetDef,
729+
) -> &'a [IdlAccountSetStructField] {
730+
let set = def
731+
.get_defined(idl)
732+
.expect("Missing referenced account set");
733+
match &set.account_set_def {
734+
IdlAccountSetDef::Struct(fields) => fields.as_slice(),
735+
other => panic!("Expected struct account set, found {other:?}"),
736+
}
737+
}
738+
739+
fn expect_field<'a>(
740+
fields: &'a [IdlAccountSetStructField],
741+
name: &str,
742+
) -> &'a IdlAccountSetStructField {
743+
fields
744+
.iter()
745+
.find(|field| field.path.as_deref() == Some(name))
746+
.unwrap_or_else(|| panic!("Missing field `{name}` in account set"))
747+
}
748+
749+
fn assert_has_program_account(field: &IdlAccountSetStructField, source: &str) {
750+
match &field.account_set_def {
751+
IdlAccountSetDef::Single(single) => {
752+
assert!(
753+
single
754+
.program_accounts
755+
.iter()
756+
.any(|account| account.source == source),
757+
"Expected `{source}` in program account list, found {:?}",
758+
single.program_accounts
759+
);
760+
}
761+
other => panic!("Expected single account set, found {other:?}"),
762+
}
763+
}
764+
765+
let idl = MintTokenTestProgram::program_to_idl()?;
766+
let instruction = idl
767+
.instructions
768+
.values()
769+
.find(|ix| ix.definition.type_id.source.ends_with("TouchSplAccounts"))
770+
.expect("Instruction not found in IDL");
771+
let fields = struct_fields(&idl, &instruction.definition.account_set);
772+
773+
let mint_source = item_source::<MintAccountData>();
774+
let token_source = item_source::<TokenAccountData>();
775+
776+
let mint_field = expect_field(fields, "mint");
777+
assert_has_program_account(mint_field, &mint_source);
778+
779+
let token_field = expect_field(fields, "token");
780+
assert_has_program_account(token_field, &token_source);
781+
782+
Ok(())
783+
}
499784

500785
#[test]
501786
fn test_mint_accessors() -> Result<()> {

0 commit comments

Comments
 (0)