diff --git a/e2e/anchor/src/generated/instructions/create_guard.rs b/e2e/anchor/src/generated/instructions/create_guard.rs index fd28a57..24002a6 100644 --- a/e2e/anchor/src/generated/instructions/create_guard.rs +++ b/e2e/anchor/src/generated/instructions/create_guard.rs @@ -124,9 +124,9 @@ impl CreateGuardInstructionArgs { /// /// ### Accounts: /// -/// 0. `[writable]` guard +/// 0. `[writable, optional]` guard (default to PDA) /// 1. `[writable, signer]` mint -/// 2. `[writable]` mint_token_account +/// 2. `[writable, optional]` mint_token_account (default to PDA) /// 3. `[signer]` guard_authority /// 4. `[writable, signer]` payer /// 5. `[optional]` associated_token_program (default to `ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL`) @@ -155,6 +155,7 @@ impl CreateGuardBuilder { pub fn new() -> Self { Self::default() } + /// `[optional account, default to PDA]` #[inline(always)] pub fn guard(&mut self, guard: solana_address::Address) -> &mut Self { self.guard = Some(guard); @@ -165,6 +166,7 @@ impl CreateGuardBuilder { self.mint = Some(mint); self } + /// `[optional account, default to PDA]` #[inline(always)] pub fn mint_token_account(&mut self, mint_token_account: solana_address::Address) -> &mut Self { self.mint_token_account = Some(mint_token_account); @@ -253,23 +255,55 @@ impl CreateGuardBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { + let mint = self.mint.expect("mint is not set"); + let guard = self.guard.unwrap_or_else(|| { + solana_address::Address::find_program_address( + &[ + &[ + 119, 101, 110, 95, 116, 111, 107, 101, 110, 95, 116, 114, 97, 110, 115, + 102, 101, 114, 95, 103, 117, 97, 114, 100, + ], + &[103, 117, 97, 114, 100, 95, 118, 49], + mint.as_ref(), + ], + &crate::WEN_TRANSFER_GUARD_ID, + ) + .0 + }); + let guard_authority = self.guard_authority.expect("guard_authority is not set"); + let token_program = self.token_program.unwrap_or(solana_address::address!( + "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + )); + let mint_token_account = self.mint_token_account.unwrap_or_else(|| { + solana_address::Address::find_program_address( + &[ + guard_authority.as_ref(), + token_program.as_ref(), + mint.as_ref(), + ], + &solana_address::address!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"), + ) + .0 + }); + let payer = self.payer.expect("payer is not set"); + let associated_token_program = + self.associated_token_program + .unwrap_or(solana_address::address!( + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + )); + let system_program = self + .system_program + .unwrap_or(solana_address::address!("11111111111111111111111111111111")); + let accounts = CreateGuard { - guard: self.guard.expect("guard is not set"), - mint: self.mint.expect("mint is not set"), - mint_token_account: self - .mint_token_account - .expect("mint_token_account is not set"), - guard_authority: self.guard_authority.expect("guard_authority is not set"), - payer: self.payer.expect("payer is not set"), - associated_token_program: self.associated_token_program.unwrap_or( - solana_address::address!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"), - ), - token_program: self.token_program.unwrap_or(solana_address::address!( - "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" - )), - system_program: self - .system_program - .unwrap_or(solana_address::address!("11111111111111111111111111111111")), + guard, + mint, + mint_token_account, + guard_authority, + payer, + associated_token_program, + token_program, + system_program, }; let args = CreateGuardInstructionArgs { name: self.name.clone().expect("name is not set"), diff --git a/e2e/anchor/src/generated/instructions/execute.rs b/e2e/anchor/src/generated/instructions/execute.rs index 410cbe4..9643769 100644 --- a/e2e/anchor/src/generated/instructions/execute.rs +++ b/e2e/anchor/src/generated/instructions/execute.rs @@ -121,7 +121,7 @@ impl ExecuteInstructionArgs { /// 1. `[]` mint /// 2. `[]` destination_account /// 3. `[]` owner_delegate -/// 4. `[]` extra_metas_account +/// 4. `[optional]` extra_metas_account (default to PDA) /// 5. `[]` guard /// 6. `[optional]` instruction_sysvar_account (default to `Sysvar1nstructions1111111111111111111111111`) #[derive(Clone, Debug, Default)] @@ -164,6 +164,7 @@ impl ExecuteBuilder { self.owner_delegate = Some(owner_delegate); self } + /// `[optional account, default to PDA]` #[inline(always)] pub fn extra_metas_account( &mut self, @@ -208,20 +209,40 @@ impl ExecuteBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { + let source_account = self.source_account.expect("source_account is not set"); + let mint = self.mint.expect("mint is not set"); + let destination_account = self + .destination_account + .expect("destination_account is not set"); + let owner_delegate = self.owner_delegate.expect("owner_delegate is not set"); + let extra_metas_account = self.extra_metas_account.unwrap_or_else(|| { + solana_address::Address::find_program_address( + &[ + &[ + 101, 120, 116, 114, 97, 45, 97, 99, 99, 111, 117, 110, 116, 45, 109, 101, + 116, 97, 115, + ], + mint.as_ref(), + ], + &crate::WEN_TRANSFER_GUARD_ID, + ) + .0 + }); + let guard = self.guard.expect("guard is not set"); + let instruction_sysvar_account = + self.instruction_sysvar_account + .unwrap_or(solana_address::address!( + "Sysvar1nstructions1111111111111111111111111" + )); + let accounts = Execute { - source_account: self.source_account.expect("source_account is not set"), - mint: self.mint.expect("mint is not set"), - destination_account: self - .destination_account - .expect("destination_account is not set"), - owner_delegate: self.owner_delegate.expect("owner_delegate is not set"), - extra_metas_account: self - .extra_metas_account - .expect("extra_metas_account is not set"), - guard: self.guard.expect("guard is not set"), - instruction_sysvar_account: self.instruction_sysvar_account.unwrap_or( - solana_address::address!("Sysvar1nstructions1111111111111111111111111"), - ), + source_account, + mint, + destination_account, + owner_delegate, + extra_metas_account, + guard, + instruction_sysvar_account, }; let args = ExecuteInstructionArgs { amount: self.amount.clone().expect("amount is not set"), diff --git a/e2e/anchor/src/generated/instructions/initialize.rs b/e2e/anchor/src/generated/instructions/initialize.rs index ce30b3c..3326ba9 100644 --- a/e2e/anchor/src/generated/instructions/initialize.rs +++ b/e2e/anchor/src/generated/instructions/initialize.rs @@ -94,7 +94,7 @@ impl Default for InitializeInstructionData { /// /// ### Accounts: /// -/// 0. `[writable]` extra_metas_account +/// 0. `[writable, optional]` extra_metas_account (default to PDA) /// 1. `[]` guard /// 2. `[]` mint /// 3. `[writable, signer]` transfer_hook_authority @@ -115,6 +115,7 @@ impl InitializeBuilder { pub fn new() -> Self { Self::default() } + /// `[optional account, default to PDA]` #[inline(always)] pub fn extra_metas_account( &mut self, @@ -169,19 +170,36 @@ impl InitializeBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { + let mint = self.mint.expect("mint is not set"); + let extra_metas_account = self.extra_metas_account.unwrap_or_else(|| { + solana_address::Address::find_program_address( + &[ + &[ + 101, 120, 116, 114, 97, 45, 97, 99, 99, 111, 117, 110, 116, 45, 109, 101, + 116, 97, 115, + ], + mint.as_ref(), + ], + &crate::WEN_TRANSFER_GUARD_ID, + ) + .0 + }); + let guard = self.guard.expect("guard is not set"); + let transfer_hook_authority = self + .transfer_hook_authority + .expect("transfer_hook_authority is not set"); + let system_program = self + .system_program + .unwrap_or(solana_address::address!("11111111111111111111111111111111")); + let payer = self.payer.expect("payer is not set"); + let accounts = Initialize { - extra_metas_account: self - .extra_metas_account - .expect("extra_metas_account is not set"), - guard: self.guard.expect("guard is not set"), - mint: self.mint.expect("mint is not set"), - transfer_hook_authority: self - .transfer_hook_authority - .expect("transfer_hook_authority is not set"), - system_program: self - .system_program - .unwrap_or(solana_address::address!("11111111111111111111111111111111")), - payer: self.payer.expect("payer is not set"), + extra_metas_account, + guard, + mint, + transfer_hook_authority, + system_program, + payer, }; accounts.instruction_with_remaining_accounts(&self.__remaining_accounts) diff --git a/e2e/anchor/src/generated/instructions/update_guard.rs b/e2e/anchor/src/generated/instructions/update_guard.rs index 9eb70a1..e10b9de 100644 --- a/e2e/anchor/src/generated/instructions/update_guard.rs +++ b/e2e/anchor/src/generated/instructions/update_guard.rs @@ -114,9 +114,9 @@ impl UpdateGuardInstructionArgs { /// /// ### Accounts: /// -/// 0. `[writable]` guard +/// 0. `[writable, optional]` guard (default to PDA) /// 1. `[]` mint -/// 2. `[]` token_account +/// 2. `[optional]` token_account (default to PDA) /// 3. `[signer]` guard_authority /// 4. `[optional]` token_program (default to `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb`) /// 5. `[optional]` system_program (default to `11111111111111111111111111111111`) @@ -138,6 +138,7 @@ impl UpdateGuardBuilder { pub fn new() -> Self { Self::default() } + /// `[optional account, default to PDA]` #[inline(always)] pub fn guard(&mut self, guard: solana_address::Address) -> &mut Self { self.guard = Some(guard); @@ -148,6 +149,7 @@ impl UpdateGuardBuilder { self.mint = Some(mint); self } + /// `[optional account, default to PDA]` #[inline(always)] pub fn token_account(&mut self, token_account: solana_address::Address) -> &mut Self { self.token_account = Some(token_account); @@ -207,17 +209,47 @@ impl UpdateGuardBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { + let mint = self.mint.expect("mint is not set"); + let guard = self.guard.unwrap_or_else(|| { + solana_address::Address::find_program_address( + &[ + &[ + 119, 101, 110, 95, 116, 111, 107, 101, 110, 95, 116, 114, 97, 110, 115, + 102, 101, 114, 95, 103, 117, 97, 114, 100, + ], + &[103, 117, 97, 114, 100, 95, 118, 49], + mint.as_ref(), + ], + &crate::WEN_TRANSFER_GUARD_ID, + ) + .0 + }); + let guard_authority = self.guard_authority.expect("guard_authority is not set"); + let token_program = self.token_program.unwrap_or(solana_address::address!( + "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + )); + let token_account = self.token_account.unwrap_or_else(|| { + solana_address::Address::find_program_address( + &[ + guard_authority.as_ref(), + token_program.as_ref(), + mint.as_ref(), + ], + &solana_address::address!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"), + ) + .0 + }); + let system_program = self + .system_program + .unwrap_or(solana_address::address!("11111111111111111111111111111111")); + let accounts = UpdateGuard { - guard: self.guard.expect("guard is not set"), - mint: self.mint.expect("mint is not set"), - token_account: self.token_account.expect("token_account is not set"), - guard_authority: self.guard_authority.expect("guard_authority is not set"), - token_program: self.token_program.unwrap_or(solana_address::address!( - "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" - )), - system_program: self - .system_program - .unwrap_or(solana_address::address!("11111111111111111111111111111111")), + guard, + mint, + token_account, + guard_authority, + token_program, + system_program, }; let args = UpdateGuardInstructionArgs { cpi_rule: self.cpi_rule.clone(), diff --git a/e2e/dummy/src/generated/instructions/instruction6.rs b/e2e/dummy/src/generated/instructions/instruction6.rs index a6762bf..0620361 100644 --- a/e2e/dummy/src/generated/instructions/instruction6.rs +++ b/e2e/dummy/src/generated/instructions/instruction6.rs @@ -93,9 +93,9 @@ impl Instruction6Builder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { - let accounts = Instruction6 { - my_account: self.my_account.expect("my_account is not set"), - }; + let my_account = self.my_account.expect("my_account is not set"); + + let accounts = Instruction6 { my_account }; accounts.instruction_with_remaining_accounts(&self.__remaining_accounts) } diff --git a/e2e/dummy/src/generated/instructions/instruction7.rs b/e2e/dummy/src/generated/instructions/instruction7.rs index fff4818..771a3f8 100644 --- a/e2e/dummy/src/generated/instructions/instruction7.rs +++ b/e2e/dummy/src/generated/instructions/instruction7.rs @@ -101,9 +101,9 @@ impl Instruction7Builder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { - let accounts = Instruction7 { - my_account: self.my_account, - }; + let my_account = self.my_account; + + let accounts = Instruction7 { my_account }; accounts.instruction_with_remaining_accounts(&self.__remaining_accounts) } diff --git a/e2e/system/src/generated/instructions/advance_nonce_account.rs b/e2e/system/src/generated/instructions/advance_nonce_account.rs index 4858fa9..53851c3 100644 --- a/e2e/system/src/generated/instructions/advance_nonce_account.rs +++ b/e2e/system/src/generated/instructions/advance_nonce_account.rs @@ -132,12 +132,18 @@ impl AdvanceNonceAccountBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { + let nonce_account = self.nonce_account.expect("nonce_account is not set"); + let recent_blockhashes_sysvar = + self.recent_blockhashes_sysvar + .unwrap_or(solana_address::address!( + "SysvarRecentB1ockHashes11111111111111111111" + )); + let nonce_authority = self.nonce_authority.expect("nonce_authority is not set"); + let accounts = AdvanceNonceAccount { - nonce_account: self.nonce_account.expect("nonce_account is not set"), - recent_blockhashes_sysvar: self.recent_blockhashes_sysvar.unwrap_or( - solana_address::address!("SysvarRecentB1ockHashes11111111111111111111"), - ), - nonce_authority: self.nonce_authority.expect("nonce_authority is not set"), + nonce_account, + recent_blockhashes_sysvar, + nonce_authority, }; accounts.instruction_with_remaining_accounts(&self.__remaining_accounts) diff --git a/e2e/system/src/generated/instructions/allocate.rs b/e2e/system/src/generated/instructions/allocate.rs index ef7989f..46ae45b 100644 --- a/e2e/system/src/generated/instructions/allocate.rs +++ b/e2e/system/src/generated/instructions/allocate.rs @@ -117,9 +117,9 @@ impl AllocateBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { - let accounts = Allocate { - new_account: self.new_account.expect("new_account is not set"), - }; + let new_account = self.new_account.expect("new_account is not set"); + + let accounts = Allocate { new_account }; let args = AllocateInstructionArgs { space: self.space.clone().expect("space is not set"), }; diff --git a/e2e/system/src/generated/instructions/allocate_with_seed.rs b/e2e/system/src/generated/instructions/allocate_with_seed.rs index 0064540..51fbfd8 100644 --- a/e2e/system/src/generated/instructions/allocate_with_seed.rs +++ b/e2e/system/src/generated/instructions/allocate_with_seed.rs @@ -158,9 +158,12 @@ impl AllocateWithSeedBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { + let new_account = self.new_account.expect("new_account is not set"); + let base_account = self.base_account.expect("base_account is not set"); + let accounts = AllocateWithSeed { - new_account: self.new_account.expect("new_account is not set"), - base_account: self.base_account.expect("base_account is not set"), + new_account, + base_account, }; let args = AllocateWithSeedInstructionArgs { base: self.base.clone().expect("base is not set"), diff --git a/e2e/system/src/generated/instructions/assign.rs b/e2e/system/src/generated/instructions/assign.rs index 4ccc263..ceda65c 100644 --- a/e2e/system/src/generated/instructions/assign.rs +++ b/e2e/system/src/generated/instructions/assign.rs @@ -118,9 +118,9 @@ impl AssignBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { - let accounts = Assign { - account: self.account.expect("account is not set"), - }; + let account = self.account.expect("account is not set"); + + let accounts = Assign { account }; let args = AssignInstructionArgs { program_address: self .program_address diff --git a/e2e/system/src/generated/instructions/assign_with_seed.rs b/e2e/system/src/generated/instructions/assign_with_seed.rs index bd88c2f..1c0aaec 100644 --- a/e2e/system/src/generated/instructions/assign_with_seed.rs +++ b/e2e/system/src/generated/instructions/assign_with_seed.rs @@ -148,9 +148,12 @@ impl AssignWithSeedBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { + let account = self.account.expect("account is not set"); + let base_account = self.base_account.expect("base_account is not set"); + let accounts = AssignWithSeed { - account: self.account.expect("account is not set"), - base_account: self.base_account.expect("base_account is not set"), + account, + base_account, }; let args = AssignWithSeedInstructionArgs { base: self.base.clone().expect("base is not set"), diff --git a/e2e/system/src/generated/instructions/authorize_nonce_account.rs b/e2e/system/src/generated/instructions/authorize_nonce_account.rs index e881144..22d16d7 100644 --- a/e2e/system/src/generated/instructions/authorize_nonce_account.rs +++ b/e2e/system/src/generated/instructions/authorize_nonce_account.rs @@ -139,9 +139,12 @@ impl AuthorizeNonceAccountBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { + let nonce_account = self.nonce_account.expect("nonce_account is not set"); + let nonce_authority = self.nonce_authority.expect("nonce_authority is not set"); + let accounts = AuthorizeNonceAccount { - nonce_account: self.nonce_account.expect("nonce_account is not set"), - nonce_authority: self.nonce_authority.expect("nonce_authority is not set"), + nonce_account, + nonce_authority, }; let args = AuthorizeNonceAccountInstructionArgs { new_nonce_authority: self diff --git a/e2e/system/src/generated/instructions/create_account.rs b/e2e/system/src/generated/instructions/create_account.rs index b455e4c..cc4203c 100644 --- a/e2e/system/src/generated/instructions/create_account.rs +++ b/e2e/system/src/generated/instructions/create_account.rs @@ -145,10 +145,10 @@ impl CreateAccountBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { - let accounts = CreateAccount { - payer: self.payer.expect("payer is not set"), - new_account: self.new_account.expect("new_account is not set"), - }; + let payer = self.payer.expect("payer is not set"); + let new_account = self.new_account.expect("new_account is not set"); + + let accounts = CreateAccount { payer, new_account }; let args = CreateAccountInstructionArgs { lamports: self.lamports.clone().expect("lamports is not set"), space: self.space.clone().expect("space is not set"), diff --git a/e2e/system/src/generated/instructions/create_account_with_seed.rs b/e2e/system/src/generated/instructions/create_account_with_seed.rs index 97676e7..5016048 100644 --- a/e2e/system/src/generated/instructions/create_account_with_seed.rs +++ b/e2e/system/src/generated/instructions/create_account_with_seed.rs @@ -177,10 +177,14 @@ impl CreateAccountWithSeedBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { + let payer = self.payer.expect("payer is not set"); + let new_account = self.new_account.expect("new_account is not set"); + let base_account = self.base_account.expect("base_account is not set"); + let accounts = CreateAccountWithSeed { - payer: self.payer.expect("payer is not set"), - new_account: self.new_account.expect("new_account is not set"), - base_account: self.base_account.expect("base_account is not set"), + payer, + new_account, + base_account, }; let args = CreateAccountWithSeedInstructionArgs { base: self.base.clone().expect("base is not set"), diff --git a/e2e/system/src/generated/instructions/initialize_nonce_account.rs b/e2e/system/src/generated/instructions/initialize_nonce_account.rs index f453f48..9b8aa23 100644 --- a/e2e/system/src/generated/instructions/initialize_nonce_account.rs +++ b/e2e/system/src/generated/instructions/initialize_nonce_account.rs @@ -157,14 +157,20 @@ impl InitializeNonceAccountBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { + let nonce_account = self.nonce_account.expect("nonce_account is not set"); + let recent_blockhashes_sysvar = + self.recent_blockhashes_sysvar + .unwrap_or(solana_address::address!( + "SysvarRecentB1ockHashes11111111111111111111" + )); + let rent_sysvar = self.rent_sysvar.unwrap_or(solana_address::address!( + "SysvarRent111111111111111111111111111111111" + )); + let accounts = InitializeNonceAccount { - nonce_account: self.nonce_account.expect("nonce_account is not set"), - recent_blockhashes_sysvar: self.recent_blockhashes_sysvar.unwrap_or( - solana_address::address!("SysvarRecentB1ockHashes11111111111111111111"), - ), - rent_sysvar: self.rent_sysvar.unwrap_or(solana_address::address!( - "SysvarRent111111111111111111111111111111111" - )), + nonce_account, + recent_blockhashes_sysvar, + rent_sysvar, }; let args = InitializeNonceAccountInstructionArgs { nonce_authority: self diff --git a/e2e/system/src/generated/instructions/transfer_sol.rs b/e2e/system/src/generated/instructions/transfer_sol.rs index 6403821..be461db 100644 --- a/e2e/system/src/generated/instructions/transfer_sol.rs +++ b/e2e/system/src/generated/instructions/transfer_sol.rs @@ -130,9 +130,12 @@ impl TransferSolBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { + let source = self.source.expect("source is not set"); + let destination = self.destination.expect("destination is not set"); + let accounts = TransferSol { - source: self.source.expect("source is not set"), - destination: self.destination.expect("destination is not set"), + source, + destination, }; let args = TransferSolInstructionArgs { amount: self.amount.clone().expect("amount is not set"), diff --git a/e2e/system/src/generated/instructions/transfer_sol_with_seed.rs b/e2e/system/src/generated/instructions/transfer_sol_with_seed.rs index 3fd3668..edb09cc 100644 --- a/e2e/system/src/generated/instructions/transfer_sol_with_seed.rs +++ b/e2e/system/src/generated/instructions/transfer_sol_with_seed.rs @@ -163,10 +163,14 @@ impl TransferSolWithSeedBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { + let source = self.source.expect("source is not set"); + let base_account = self.base_account.expect("base_account is not set"); + let destination = self.destination.expect("destination is not set"); + let accounts = TransferSolWithSeed { - source: self.source.expect("source is not set"), - base_account: self.base_account.expect("base_account is not set"), - destination: self.destination.expect("destination is not set"), + source, + base_account, + destination, }; let args = TransferSolWithSeedInstructionArgs { amount: self.amount.clone().expect("amount is not set"), diff --git a/e2e/system/src/generated/instructions/upgrade_nonce_account.rs b/e2e/system/src/generated/instructions/upgrade_nonce_account.rs index 7de98bc..d438d12 100644 --- a/e2e/system/src/generated/instructions/upgrade_nonce_account.rs +++ b/e2e/system/src/generated/instructions/upgrade_nonce_account.rs @@ -102,9 +102,9 @@ impl UpgradeNonceAccountBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { - let accounts = UpgradeNonceAccount { - nonce_account: self.nonce_account.expect("nonce_account is not set"), - }; + let nonce_account = self.nonce_account.expect("nonce_account is not set"); + + let accounts = UpgradeNonceAccount { nonce_account }; accounts.instruction_with_remaining_accounts(&self.__remaining_accounts) } diff --git a/e2e/system/src/generated/instructions/withdraw_nonce_account.rs b/e2e/system/src/generated/instructions/withdraw_nonce_account.rs index da43d7d..9c02fd4 100644 --- a/e2e/system/src/generated/instructions/withdraw_nonce_account.rs +++ b/e2e/system/src/generated/instructions/withdraw_nonce_account.rs @@ -182,18 +182,26 @@ impl WithdrawNonceAccountBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { + let nonce_account = self.nonce_account.expect("nonce_account is not set"); + let recipient_account = self + .recipient_account + .expect("recipient_account is not set"); + let recent_blockhashes_sysvar = + self.recent_blockhashes_sysvar + .unwrap_or(solana_address::address!( + "SysvarRecentB1ockHashes11111111111111111111" + )); + let rent_sysvar = self.rent_sysvar.unwrap_or(solana_address::address!( + "SysvarRent111111111111111111111111111111111" + )); + let nonce_authority = self.nonce_authority.expect("nonce_authority is not set"); + let accounts = WithdrawNonceAccount { - nonce_account: self.nonce_account.expect("nonce_account is not set"), - recipient_account: self - .recipient_account - .expect("recipient_account is not set"), - recent_blockhashes_sysvar: self.recent_blockhashes_sysvar.unwrap_or( - solana_address::address!("SysvarRecentB1ockHashes11111111111111111111"), - ), - rent_sysvar: self.rent_sysvar.unwrap_or(solana_address::address!( - "SysvarRent111111111111111111111111111111111" - )), - nonce_authority: self.nonce_authority.expect("nonce_authority is not set"), + nonce_account, + recipient_account, + recent_blockhashes_sysvar, + rent_sysvar, + nonce_authority, }; let args = WithdrawNonceAccountInstructionArgs { withdraw_amount: self diff --git a/public/templates/instructionsPageBuilder.njk b/public/templates/instructionsPageBuilder.njk index 44b3682..9c0074d 100644 --- a/public/templates/instructionsPageBuilder.njk +++ b/public/templates/instructionsPageBuilder.njk @@ -10,11 +10,12 @@ {% if account.isSigner %} {% set modifiers = modifiers + ', signer' if modifiers.length > 0 else 'signer' %} {% endif %} - {% if account.isOptional or account.defaultValue.kind === 'publicKeyValueNode' %} + {% if account.isOptional or account.defaultValue.kind === 'publicKeyValueNode' or account.defaultValue.kind === 'pdaValueNode' %} {% set modifiers = modifiers + ', optional' if modifiers.length > 0 else 'optional' %} {% endif %} {{ '/// ' + loop.index0 + '. `[' + modifiers + ']` ' + account.name | snakeCase }} {{- " (default to `" + account.defaultValue.publicKey + "`)" if account.defaultValue.kind === 'publicKeyValueNode' }} + {{- " (default to PDA)" if account.defaultValue.kind === 'pdaValueNode' }} {% endfor %} #[derive(Clone, Debug, Default)] pub struct {{ instruction.name | pascalCase }}Builder { @@ -40,8 +41,10 @@ impl {{ instruction.name | pascalCase }}Builder { {% for account in instruction.accounts %} {% if account.isOptional %} {{ '/// `[optional account]`\n' -}} - {% else %} - {{ "/// `[optional account, default to '" + account.defaultValue.publicKey + "']`\n" if account.defaultValue.kind === 'publicKeyValueNode' -}} + {% elif account.defaultValue.kind === 'publicKeyValueNode' %} + {{ "/// `[optional account, default to '" + account.defaultValue.publicKey + "']`\n" -}} + {% elif account.defaultValue.kind === 'pdaValueNode' %} + {{ '/// `[optional account, default to PDA]`\n' -}} {% endif %} {{- macros.docblock(account.docs) -}} #[inline(always)] @@ -92,17 +95,48 @@ impl {{ instruction.name | pascalCase }}Builder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_instruction::Instruction { + {% for account in resolvedAccounts %} + {% if account.isOptional %} + let {{ account.name | snakeCase }} = self.{{ account.name | snakeCase }}; + {% elif account.defaultValue.kind === 'programId' %} + let {{ account.name | snakeCase }} = self.{{ account.name | snakeCase }}; {# Program ID set on the instruction creation. #} + {% elif account.pdaDefault %} + {% if account.pdaDefault.isLinked and account.pdaDefault.linkedAccountName %} + let {{ account.name | snakeCase }} = self.{{ account.name | snakeCase }}.unwrap_or_else(|| { + {{ account.pdaDefault.linkedAccountName | pascalCase }}::find_pda( + {% for seed in account.pdaDefault.renderedSeeds %} + {% if seed.kind === 'accountRef' %} + &{{ seed.rawName }}, + {% elif seed.kind === 'argumentRef' %} + {{ seed.render }}, + {% elif seed.kind === 'value' %} + {{ seed.render }}, + {% endif %} + {% endfor %} + ).0 + }); + {% else %} + let {{ account.name | snakeCase }} = self.{{ account.name | snakeCase }}.unwrap_or_else(|| { + solana_address::Address::find_program_address( + &[ + {% for seed in account.pdaDefault.renderedSeeds %} + {{ seed.render }}, + {% endfor %} + ], + &{{ account.pdaDefault.programAddressExpr }}, + ).0 + }); + {% endif %} + {% elif account.defaultValue.kind === 'publicKeyValueNode' %} + let {{ account.name | snakeCase }} = self.{{ account.name | snakeCase }}.unwrap_or(solana_address::address!("{{ account.defaultValue.publicKey }}")); + {% else %} + let {{ account.name | snakeCase }} = self.{{ account.name | snakeCase }}.expect("{{ account.name | snakeCase }} is not set"); + {% endif %} + {% endfor %} + let accounts = {{ instruction.name | pascalCase }} { {% for account in instruction.accounts %} - {% if account.isOptional %} - {{ account.name | snakeCase }}: self.{{ account.name | snakeCase }}, - {% elif account.defaultValue.kind === 'programId' %} - {{ account.name | snakeCase }}: self.{{ account.name | snakeCase }}, {# Program ID set on the instruction creation. #} - {% elif account.defaultValue.kind === 'publicKeyValueNode' %} - {{ account.name | snakeCase }}: self.{{ account.name | snakeCase }}.unwrap_or(solana_address::address!("{{ account.defaultValue.publicKey }}")), - {% else %} - {{ account.name | snakeCase }}: self.{{ account.name | snakeCase }}.expect("{{ account.name | snakeCase }} is not set"), - {% endif %} + {{ account.name | snakeCase }}, {% endfor %} }; {% if hasArgs %} diff --git a/src/getRenderMapVisitor.ts b/src/getRenderMapVisitor.ts index 55e65a1..708f17a 100644 --- a/src/getRenderMapVisitor.ts +++ b/src/getRenderMapVisitor.ts @@ -1,13 +1,16 @@ import { logWarn } from '@codama/errors'; import { + camelCase, getAllAccounts, getAllDefinedTypes, getAllInstructionsWithSubs, getAllPrograms, + type InstructionAccountNode, InstructionNode, isNode, isNodeFilter, pascalCase, + PdaNode, ProgramNode, resolveNestedTypeNode, snakeCase, @@ -34,6 +37,7 @@ import { Fragment, getDiscriminatorConstants, getImportFromFactory, + type GetImportFromFunction, getTraitsFromNodeFactory, LinkOverrides, render, @@ -233,6 +237,17 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) { }); const typeManifest = visit(struct, structVisitor); + // Resolve PDA defaults and topologically sort accounts by dependency. + const resolvedAccounts = resolveInstructionPdaDefaults({ + accounts: node.accounts, + getImportFrom, + imports, + instructionName: node.name, + linkables, + program: program!, + stack, + }); + const dataTraits = getTraitsFromNode(node); imports .mergeWith(dataTraits.imports) @@ -249,6 +264,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) { instruction: node, instructionArgs, program, + resolvedAccounts, typeManifest, }), imports, @@ -340,6 +356,249 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) { ); } +type RenderedSeed = { + kind: 'accountRef' | 'argumentRef' | 'constant' | 'programId' | 'value'; + rawName?: string; + render: string; +}; + +type ResolvedPdaInfo = { + accountDeps: string[]; + isLinked: boolean; + linkedAccountName?: string; + linkedImportFrom?: string; + programAddressExpr: string; + renderedSeeds: RenderedSeed[]; +}; + +type ResolvedAccount = InstructionAccountNode & { pdaDefault: ResolvedPdaInfo | null }; + +function resolveInstructionPdaDefaults(ctx: { + accounts: readonly InstructionAccountNode[]; + getImportFrom: GetImportFromFunction; + imports: ImportMap; + instructionName: string; + linkables: LinkableDictionary; + program: ProgramNode; + stack: NodeStack; +}): ResolvedAccount[] { + const { accounts, getImportFrom, imports, instructionName, linkables, program, stack } = ctx; + + const resolvedPdaAccounts: Record = {}; + + for (const account of accounts) { + if (!account.defaultValue || !isNode(account.defaultValue, 'pdaValueNode')) { + continue; + } + + const defaultValue = account.defaultValue; + let pdaNode: PdaNode | undefined; + let isLinked = false; + let linkedAccountName: string | undefined; + let linkedImportFrom: string | undefined; + + if (isNode(defaultValue.pda, 'pdaLinkNode')) { + pdaNode = linkables.get([...stack.getPath(), defaultValue.pda]); + if (pdaNode) { + // Only use the linked find_pda() path if there's an account struct + // that references this PDA (since find_pda is generated on account structs). + const linkedAccount = program.accounts.find( + a => a.pda && isNode(a.pda, 'pdaLinkNode') && a.pda.name === defaultValue.pda.name, + ); + if (linkedAccount) { + isLinked = true; + linkedAccountName = linkedAccount.name; + linkedImportFrom = getImportFrom(defaultValue.pda); + } + } + } else if (isNode(defaultValue.pda, 'pdaNode')) { + pdaNode = defaultValue.pda; + } + + if (!pdaNode) { + logWarn( + `[Rust] Could not resolve PDA node for account [${account.name}] ` + + `in instruction [${instructionName}]. The account will be treated as required.`, + ); + continue; + } + + // Resolve programId: check pdaValueNode override, then pdaNode.programId, then default. + let programIdOverride: string | undefined; + if (isNode(defaultValue.programId, 'accountValueNode')) { + programIdOverride = snakeCase(defaultValue.programId.name); + } else if (isNode(defaultValue.programId, 'argumentValueNode')) { + programIdOverride = snakeCase(defaultValue.programId.name); + } else if (pdaNode.programId) { + programIdOverride = `solana_address::address!("${pdaNode.programId}")`; + } + + const programAddressExpr = programIdOverride ?? `crate::${snakeCase(program.name).toUpperCase()}_ID`; + + // Render seeds — bail out entirely if any variable seed value is missing. + const renderedSeeds: RenderedSeed[] = []; + const seedAccountDeps: string[] = []; + let seedsComplete = true; + + if (isNode(defaultValue.programId, 'accountValueNode')) { + seedAccountDeps.push(camelCase(defaultValue.programId.name)); + } + + for (const seed of pdaNode.seeds) { + if (isNode(seed, 'constantPdaSeedNode')) { + if (isNode(seed.value, 'programIdValueNode')) { + renderedSeeds.push({ + kind: 'programId', + render: `${programAddressExpr}.as_ref()`, + }); + } else { + const valueManifest = renderValueNode(seed.value, getImportFrom, true); + imports.mergeWith(valueManifest.imports); + const rendered = valueManifest.render; + const suffix = rendered.startsWith('[') ? '' : '.as_bytes()'; + const prefix = rendered.startsWith('[') ? '&' : ''; + renderedSeeds.push({ + kind: 'constant', + render: `${prefix}${rendered}${suffix}`, + }); + } + } else if (isNode(seed, 'variablePdaSeedNode')) { + const seedValue = defaultValue.seeds.find(s => s.name === seed.name)?.value; + + if (!seedValue) { + logWarn( + `[Rust] Missing seed value for variable seed [${seed.name}] ` + + `in PDA default for account [${account.name}] ` + + `of instruction [${instructionName}]. Skipping PDA resolution.`, + ); + seedsComplete = false; + break; + } + + const resolvedType = resolveNestedTypeNode(seed.type); + if (isNode(seedValue, 'accountValueNode')) { + const refName = snakeCase(seedValue.name); + seedAccountDeps.push(camelCase(seedValue.name)); + if (resolvedType.kind === 'publicKeyTypeNode') { + renderedSeeds.push({ + kind: 'accountRef', + rawName: refName, + render: `${refName}.as_ref()`, + }); + } else if (resolvedType.kind === 'bytesTypeNode') { + renderedSeeds.push({ + kind: 'accountRef', + rawName: refName, + render: `&${refName}`, + }); + } else { + renderedSeeds.push({ + kind: 'accountRef', + rawName: refName, + render: `${refName}.to_string().as_ref()`, + }); + } + } else if (isNode(seedValue, 'argumentValueNode')) { + const refName = snakeCase(seedValue.name); + if (resolvedType.kind === 'publicKeyTypeNode') { + renderedSeeds.push({ + kind: 'argumentRef', + render: `self.${refName}.as_ref().expect("${refName} is not set").as_ref()`, + }); + } else { + renderedSeeds.push({ + kind: 'argumentRef', + render: `self.${refName}.as_ref().expect("${refName} is not set").to_string().as_ref()`, + }); + } + } else { + const valueManifest = renderValueNode(seedValue, getImportFrom, true); + imports.mergeWith(valueManifest.imports); + if (resolvedType.kind === 'publicKeyTypeNode') { + renderedSeeds.push({ + kind: 'value', + render: `${valueManifest.render}.as_ref()`, + }); + } else { + renderedSeeds.push({ + kind: 'value', + render: `${valueManifest.render}.as_bytes()`, + }); + } + } + } + } + + if (!seedsComplete) continue; + + if (isLinked && linkedImportFrom && linkedAccountName) { + imports.add(`${linkedImportFrom}::${pascalCase(linkedAccountName)}`); + } + + resolvedPdaAccounts[camelCase(account.name)] = { + accountDeps: seedAccountDeps, + isLinked, + linkedAccountName, + linkedImportFrom, + programAddressExpr, + renderedSeeds, + }; + } + + // Build dependency graph and topologically sort accounts. + const accountDeps = new Map>(); + for (const account of accounts) { + const name = camelCase(account.name); + accountDeps.set(name, new Set()); + const pdaInfo = resolvedPdaAccounts[name]; + if (pdaInfo) { + for (const dep of pdaInfo.accountDeps) { + accountDeps.get(name)!.add(dep); + } + } + } + + const sortedAccountNames: string[] = []; + const visited = new Set(); + const visiting = new Set(); + + const topoSort = (name: string): boolean => { + if (visited.has(name)) return true; + if (visiting.has(name)) { + logWarn( + `[Rust] Circular PDA dependency detected for account [${name}] ` + + `in instruction [${instructionName}]. Falling back to required account.`, + ); + delete resolvedPdaAccounts[name]; + return false; + } + visiting.add(name); + const deps = accountDeps.get(name) ?? new Set(); + for (const dep of deps) { + if (accountDeps.has(dep) && !topoSort(dep)) { + // Dependency lost its PDA resolution — remove ours too. + delete resolvedPdaAccounts[name]; + } + } + visiting.delete(name); + visited.add(name); + sortedAccountNames.push(name); + return resolvedPdaAccounts[name] !== undefined || !accountDeps.get(name)?.size; + }; + + for (const account of accounts) { + topoSort(camelCase(account.name)); + } + + return sortedAccountNames.map(name => { + const account = accounts.find(a => camelCase(a.name) === name)!; + return { + ...account, + pdaDefault: resolvedPdaAccounts[name] ?? null, + }; + }); +} + function getConflictsForInstructionAccountsAndArgs(instruction: InstructionNode): string[] { const allNames = [ ...instruction.accounts.map(account => account.name), diff --git a/test/instructionsPage.test.ts b/test/instructionsPage.test.ts index 8d1ad3a..61360d6 100644 --- a/test/instructionsPage.test.ts +++ b/test/instructionsPage.test.ts @@ -1,10 +1,29 @@ -import { instructionArgumentNode, instructionNode, programNode, stringTypeNode } from '@codama/nodes'; +import { + accountNode, + accountValueNode, + argumentValueNode, + bytesTypeNode, + constantPdaSeedNodeFromProgramId, + constantPdaSeedNodeFromString, + instructionAccountNode, + instructionArgumentNode, + instructionNode, + numberTypeNode, + pdaLinkNode, + pdaNode, + pdaSeedValueNode, + pdaValueNode, + programNode, + publicKeyTypeNode, + stringTypeNode, + variablePdaSeedNode, +} from '@codama/nodes'; import { getFromRenderMap } from '@codama/renderers-core'; import { visit } from '@codama/visitors-core'; -import { test } from 'vitest'; +import { expect, test } from 'vitest'; import { getRenderMapVisitor } from '../src'; -import { codeContains } from './_setup'; +import { codeContains, codeDoesNotContains } from './_setup'; test('it renders a public instruction data struct', () => { // Given the following program with 1 instruction. @@ -69,3 +88,561 @@ test('it renders a default impl for instruction data struct', () => { `fn default(`, ]); }); + +test('it resolves inline pdaValueNode defaults with constant seeds', () => { + // Given an instruction with an account that defaults to an inline PDA with constant seeds. + const node = programNode({ + instructions: [ + instructionNode({ + accounts: [ + instructionAccountNode({ + defaultValue: pdaValueNode( + pdaNode({ + name: 'eventAuthority', + seeds: [constantPdaSeedNodeFromString('utf8', '__event_authority')], + }), + ), + isSigner: false, + isWritable: false, + name: 'eventAuthority', + }), + ], + name: 'emitEvent', + }), + ], + name: 'myProgram', + publicKey: '1111111111111111111111111111111111111111111', + }); + + // When we render it. + const renderMap = visit(node, getRenderMapVisitor()); + const content = getFromRenderMap(renderMap, 'instructions/emit_event.rs').content; + + // Then the builder resolves the PDA using find_program_address. + codeContains(content, [ + 'unwrap_or_else', + 'find_program_address', + '"__event_authority".as_bytes()', + 'MY_PROGRAM_ID', + ]); +}); + +test('it resolves linked pdaValueNode defaults', () => { + // Given an instruction with an account that defaults to a linked PDA. + const node = programNode({ + accounts: [ + accountNode({ + name: 'testAccount', + pda: pdaLinkNode('testPda'), + }), + ], + instructions: [ + instructionNode({ + accounts: [ + instructionAccountNode({ + defaultValue: pdaValueNode('testPda'), + isSigner: false, + isWritable: false, + name: 'testAccount', + }), + ], + name: 'doSomething', + }), + ], + name: 'myProgram', + pdas: [pdaNode({ name: 'testPda', seeds: [constantPdaSeedNodeFromString('utf8', 'seed')] })], + publicKey: '1111111111111111111111111111111111111111111', + }); + + // When we render it. + const renderMap = visit(node, getRenderMapVisitor()); + const content = getFromRenderMap(renderMap, 'instructions/do_something.rs').content; + + // Then the builder calls the account's find_pda method (using the account name, not PDA name). + codeContains(content, ['unwrap_or_else', 'TestAccount::find_pda']); +}); + +test('it resolves pdaValueNode defaults with variable seeds referencing accounts', () => { + // Given an instruction with accounts where a PDA seed references another account. + const node = programNode({ + instructions: [ + instructionNode({ + accounts: [ + instructionAccountNode({ + isSigner: false, + isWritable: false, + name: 'owner', + }), + instructionAccountNode({ + defaultValue: pdaValueNode( + pdaNode({ + name: 'myPda', + seeds: [ + constantPdaSeedNodeFromString('utf8', 'token'), + variablePdaSeedNode('owner', publicKeyTypeNode()), + ], + }), + [pdaSeedValueNode('owner', accountValueNode('owner'))], + ), + isSigner: false, + isWritable: false, + name: 'tokenAccount', + }), + ], + name: 'transfer', + }), + ], + name: 'myProgram', + publicKey: '1111111111111111111111111111111111111111111', + }); + + // When we render it. + const renderMap = visit(node, getRenderMapVisitor()); + const content = getFromRenderMap(renderMap, 'instructions/transfer.rs').content; + + // Then the builder resolves the PDA with the owner account as a seed. + codeContains(content, ['find_program_address', '"token".as_bytes()', 'owner.as_ref()']); +}); + +test('it orders accounts by dependency for PDA resolution', () => { + // Given an instruction where the PDA account depends on another account. + const node = programNode({ + instructions: [ + instructionNode({ + accounts: [ + // PDA account listed BEFORE its dependency. + instructionAccountNode({ + defaultValue: pdaValueNode( + pdaNode({ + name: 'derivedPda', + seeds: [variablePdaSeedNode('base', publicKeyTypeNode())], + }), + [pdaSeedValueNode('base', accountValueNode('base'))], + ), + isSigner: false, + isWritable: false, + name: 'derived', + }), + instructionAccountNode({ + isSigner: false, + isWritable: false, + name: 'base', + }), + ], + name: 'init', + }), + ], + name: 'myProgram', + publicKey: '1111111111111111111111111111111111111111111', + }); + + // When we render it. + const renderMap = visit(node, getRenderMapVisitor()); + const content = getFromRenderMap(renderMap, 'instructions/init.rs').content; + + // Then the base account is resolved before the derived PDA account. + const baseIdx = content.indexOf('let base ='); + const derivedIdx = content.indexOf('let derived ='); + codeContains(content, ['let base =', 'let derived =']); + expect(baseIdx).toBeLessThan(derivedIdx); +}); + +test('it marks pdaValueNode accounts as optional in docblock', () => { + // Given an instruction with a PDA-defaulted account. + const node = programNode({ + instructions: [ + instructionNode({ + accounts: [ + instructionAccountNode({ + defaultValue: pdaValueNode( + pdaNode({ + name: 'authority', + seeds: [constantPdaSeedNodeFromString('utf8', 'auth')], + }), + ), + isSigner: false, + isWritable: false, + name: 'authority', + }), + ], + name: 'doStuff', + }), + ], + name: 'myProgram', + publicKey: '1111111111111111111111111111111111111111111', + }); + + // When we render it. + const renderMap = visit(node, getRenderMapVisitor()); + const content = getFromRenderMap(renderMap, 'instructions/do_stuff.rs').content; + + // Then the docblock marks it as optional with PDA default. + codeContains(content, ['optional', 'default to PDA']); +}); + +test('it resolves pdaValueNode defaults with argument seeds', () => { + // Given a PDA with a variable seed referencing an instruction argument. + const node = programNode({ + instructions: [ + instructionNode({ + accounts: [ + instructionAccountNode({ + defaultValue: pdaValueNode( + pdaNode({ + name: 'lookup', + seeds: [ + constantPdaSeedNodeFromString('utf8', 'lookup'), + variablePdaSeedNode('id', numberTypeNode('u64')), + ], + }), + [pdaSeedValueNode('id', argumentValueNode('lookupId'))], + ), + isSigner: false, + isWritable: false, + name: 'lookupAccount', + }), + ], + arguments: [ + instructionArgumentNode({ + name: 'lookupId', + type: numberTypeNode('u64'), + }), + ], + name: 'lookup', + }), + ], + name: 'myProgram', + publicKey: '1111111111111111111111111111111111111111111', + }); + + const renderMap = visit(node, getRenderMapVisitor()); + const content = getFromRenderMap(renderMap, 'instructions/lookup.rs').content; + + // Then the builder uses the argument value as a seed. + codeContains(content, [ + 'unwrap_or_else', + 'find_program_address', + '"lookup".as_bytes()', + 'self.lookup_id.as_ref().expect("lookup_id is not set")', + ]); +}); + +test('it uses pdaNode.programId for inline PDAs with custom program', () => { + // Given an inline PDA with a hardcoded programId. + const node = programNode({ + instructions: [ + instructionNode({ + accounts: [ + instructionAccountNode({ + defaultValue: pdaValueNode( + pdaNode({ + name: 'crossPda', + programId: 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL', + seeds: [constantPdaSeedNodeFromString('utf8', 'cross')], + }), + ), + isSigner: false, + isWritable: false, + name: 'crossAccount', + }), + ], + name: 'crossProgram', + }), + ], + name: 'myProgram', + publicKey: '1111111111111111111111111111111111111111111', + }); + + const renderMap = visit(node, getRenderMapVisitor()); + const content = getFromRenderMap(renderMap, 'instructions/cross_program.rs').content; + + // Then it uses the PDA's custom program address, not the current program. + codeContains(content, ['find_program_address', 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL']); + // The PDA derivation should NOT use the current program's ID. + codeDoesNotContains(content, ['&crate::MY_PROGRAM_ID']); +}); + +test('it renders programIdValueNode constant seeds', () => { + // Given a PDA with a programIdValueNode seed (program address used as seed bytes). + const node = programNode({ + instructions: [ + instructionNode({ + accounts: [ + instructionAccountNode({ + defaultValue: pdaValueNode( + pdaNode({ + name: 'programData', + seeds: [ + constantPdaSeedNodeFromString('utf8', 'data'), + constantPdaSeedNodeFromProgramId(), + ], + }), + ), + isSigner: false, + isWritable: false, + name: 'programData', + }), + ], + name: 'readData', + }), + ], + name: 'myProgram', + publicKey: '1111111111111111111111111111111111111111111', + }); + + const renderMap = visit(node, getRenderMapVisitor()); + const content = getFromRenderMap(renderMap, 'instructions/read_data.rs').content; + + // Then the programId seed renders using the program constant. + codeContains(content, ['find_program_address', '"data".as_bytes()', 'MY_PROGRAM_ID.as_ref()']); +}); + +test('it renders bytesTypeNode variable seeds with & prefix', () => { + // Given a PDA with a variable seed of bytesTypeNode. + const node = programNode({ + instructions: [ + instructionNode({ + accounts: [ + instructionAccountNode({ + isSigner: false, + isWritable: false, + name: 'data', + }), + instructionAccountNode({ + defaultValue: pdaValueNode( + pdaNode({ + name: 'derived', + seeds: [variablePdaSeedNode('rawData', bytesTypeNode())], + }), + [pdaSeedValueNode('rawData', accountValueNode('data'))], + ), + isSigner: false, + isWritable: false, + name: 'derived', + }), + ], + name: 'process', + }), + ], + name: 'myProgram', + publicKey: '1111111111111111111111111111111111111111111', + }); + + const renderMap = visit(node, getRenderMapVisitor()); + const content = getFromRenderMap(renderMap, 'instructions/process.rs').content; + + // Then the bytes seed uses & prefix, not .as_ref(). + codeContains(content, ['&data,']); + codeDoesNotContains(content, ['data.as_ref()']); +}); + +test('it resolves linked pdaValueNode with variable account seeds', () => { + // Given a linked PDA whose find_pda takes an account reference. + const node = programNode({ + accounts: [ + accountNode({ + name: 'userToken', + pda: pdaLinkNode('userTokenPda'), + }), + ], + instructions: [ + instructionNode({ + accounts: [ + instructionAccountNode({ + isSigner: false, + isWritable: false, + name: 'owner', + }), + instructionAccountNode({ + defaultValue: pdaValueNode('userTokenPda', [ + pdaSeedValueNode('owner', accountValueNode('owner')), + ]), + isSigner: false, + isWritable: false, + name: 'userToken', + }), + ], + name: 'claim', + }), + ], + name: 'myProgram', + pdas: [ + pdaNode({ + name: 'userTokenPda', + seeds: [ + constantPdaSeedNodeFromString('utf8', 'token'), + variablePdaSeedNode('owner', publicKeyTypeNode()), + ], + }), + ], + publicKey: '1111111111111111111111111111111111111111111', + }); + + const renderMap = visit(node, getRenderMapVisitor()); + const content = getFromRenderMap(renderMap, 'instructions/claim.rs').content; + + // Then the linked find_pda uses the account name (not PDA name) and receives the account reference. + codeContains(content, ['UserToken::find_pda', '&owner,']); +}); + +test('it falls back to .expect() on circular PDA dependencies', () => { + // Given two accounts with circular PDA dependencies (A depends on B, B depends on A). + const node = programNode({ + instructions: [ + instructionNode({ + accounts: [ + instructionAccountNode({ + defaultValue: pdaValueNode( + pdaNode({ + name: 'pdaA', + seeds: [variablePdaSeedNode('b', publicKeyTypeNode())], + }), + [pdaSeedValueNode('b', accountValueNode('accountB'))], + ), + isSigner: false, + isWritable: false, + name: 'accountA', + }), + instructionAccountNode({ + defaultValue: pdaValueNode( + pdaNode({ + name: 'pdaB', + seeds: [variablePdaSeedNode('a', publicKeyTypeNode())], + }), + [pdaSeedValueNode('a', accountValueNode('accountA'))], + ), + isSigner: false, + isWritable: false, + name: 'accountB', + }), + ], + name: 'circular', + }), + ], + name: 'myProgram', + publicKey: '1111111111111111111111111111111111111111111', + }); + + const renderMap = visit(node, getRenderMapVisitor()); + const content = getFromRenderMap(renderMap, 'instructions/circular.rs').content; + + // Then both accounts fall back to .expect() since neither PDA can be resolved. + codeContains(content, [ + 'account_a = self.account_a.expect("account_a is not set")', + 'account_b = self.account_b.expect("account_b is not set")', + ]); + codeDoesNotContains(content, ['find_program_address', 'unwrap_or_else']); +}); + +test('it uses optional path (not PDA) when account is isOptional with PDA default', () => { + // Given an account that is both isOptional and has a PDA default. + const node = programNode({ + instructions: [ + instructionNode({ + accounts: [ + instructionAccountNode({ + defaultValue: pdaValueNode( + pdaNode({ + name: 'optPda', + seeds: [constantPdaSeedNodeFromString('utf8', 'opt')], + }), + ), + isOptional: true, + isSigner: false, + isWritable: false, + name: 'optionalAccount', + }), + ], + name: 'maybeUse', + }), + ], + name: 'myProgram', + publicKey: '1111111111111111111111111111111111111111111', + }); + + const renderMap = visit(node, getRenderMapVisitor()); + const content = getFromRenderMap(renderMap, 'instructions/maybe_use.rs').content; + + // Then the optional flag takes precedence — no PDA auto-derivation. + codeContains(content, ['let optional_account = self.optional_account;']); + codeDoesNotContains(content, ['find_program_address', 'unwrap_or_else']); +}); + +test('it inlines find_program_address for linked PDA without matching account struct', () => { + // Given a PDA defined in program.pdas but with NO corresponding account in program.accounts. + const node = programNode({ + instructions: [ + instructionNode({ + accounts: [ + instructionAccountNode({ + defaultValue: pdaValueNode('pdaOnly', []), + isSigner: false, + isWritable: false, + name: 'derivedAccount', + }), + ], + name: 'useIt', + }), + ], + name: 'myProgram', + pdas: [pdaNode({ name: 'pdaOnly', seeds: [constantPdaSeedNodeFromString('utf8', 'pda_only')] })], + publicKey: '1111111111111111111111111111111111111111111', + }); + + const renderMap = visit(node, getRenderMapVisitor()); + const content = getFromRenderMap(renderMap, 'instructions/use_it.rs').content; + + // Then it falls back to inline find_program_address (not find_pda). + codeContains(content, ['find_program_address', '"pda_only".as_bytes()']); + codeDoesNotContains(content, ['::find_pda']); +}); + +test('it uses the account name (not PDA name) for linked find_pda when names differ', () => { + // Given an account named differently from its PDA. + const node = programNode({ + accounts: [ + accountNode({ + name: 'extensionsHeader', + pda: pdaLinkNode('extensions'), + }), + ], + instructions: [ + instructionNode({ + accounts: [ + instructionAccountNode({ + isSigner: false, + isWritable: false, + name: 'owner', + }), + instructionAccountNode({ + defaultValue: pdaValueNode('extensions', [ + pdaSeedValueNode('owner', accountValueNode('owner')), + ]), + isSigner: false, + isWritable: false, + name: 'ext', + }), + ], + name: 'readExtensions', + }), + ], + name: 'myProgram', + pdas: [ + pdaNode({ + name: 'extensions', + seeds: [ + constantPdaSeedNodeFromString('utf8', 'ext'), + variablePdaSeedNode('owner', publicKeyTypeNode()), + ], + }), + ], + publicKey: '1111111111111111111111111111111111111111111', + }); + + const renderMap = visit(node, getRenderMapVisitor()); + const content = getFromRenderMap(renderMap, 'instructions/read_extensions.rs').content; + + // Then it calls ExtensionsHeader::find_pda (the account name), not Extensions::find_pda. + codeContains(content, ['ExtensionsHeader::find_pda']); + codeDoesNotContains(content, ['Extensions::find_pda']); +});