From 367c00f48f3bbcbac9c0fab51ed8efa1867435b3 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Thu, 19 Feb 2026 02:04:01 +0000 Subject: [PATCH 01/26] fix(token-sdk, token-pinocchio): make authority/owner readonly in close, approve, revoke MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit close: owner changed from writable to readonly — on-chain uses next_signer (signer-only check), never writes to owner account. approve/revoke: added max_top_up: Option field. Owner is now readonly by default (max_top_up: None) and only writable when max_top_up is Some, matching the existing pattern in transfer and burn. This prevents privilege escalation failures when calling programs pass authority as read-only. --- sdk-libs/token-client/src/actions/approve.rs | 2 ++ sdk-libs/token-client/src/actions/revoke.rs | 2 ++ .../src/instruction/approve.rs | 21 ++++++++++++++---- .../token-pinocchio/src/instruction/revoke.rs | 22 +++++++++++++++---- sdk-libs/token-sdk/src/instruction/approve.rs | 18 ++++++++++++++- sdk-libs/token-sdk/src/instruction/close.rs | 2 +- sdk-libs/token-sdk/src/instruction/revoke.rs | 22 +++++++++++++++++-- sdk-libs/token-sdk/tests/instruction_close.rs | 4 ++-- .../sdk-light-token-pinocchio/src/approve.rs | 2 ++ .../sdk-light-token-pinocchio/src/revoke.rs | 2 ++ sdk-tests/sdk-light-token-test/src/approve.rs | 2 ++ sdk-tests/sdk-light-token-test/src/revoke.rs | 2 ++ 12 files changed, 87 insertions(+), 14 deletions(-) diff --git a/sdk-libs/token-client/src/actions/approve.rs b/sdk-libs/token-client/src/actions/approve.rs index 547acb1b1d..b86ae80ef7 100644 --- a/sdk-libs/token-client/src/actions/approve.rs +++ b/sdk-libs/token-client/src/actions/approve.rs @@ -79,6 +79,7 @@ impl Approve { delegate: self.delegate, owner: owner_pubkey, amount: self.amount, + max_top_up: None, } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; @@ -121,6 +122,7 @@ impl Approve { delegate: self.delegate, owner: owner.pubkey(), amount: self.amount, + max_top_up: None, } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; diff --git a/sdk-libs/token-client/src/actions/revoke.rs b/sdk-libs/token-client/src/actions/revoke.rs index 803b4516ad..cab0408e78 100644 --- a/sdk-libs/token-client/src/actions/revoke.rs +++ b/sdk-libs/token-client/src/actions/revoke.rs @@ -69,6 +69,7 @@ impl Revoke { let ix = RevokeInstruction { token_account: self.token_account, owner: owner_pubkey, + max_top_up: None, } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; @@ -95,6 +96,7 @@ impl Revoke { let ix = RevokeInstruction { token_account: self.token_account, owner: owner.pubkey(), + max_top_up: None, } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; diff --git a/sdk-libs/token-pinocchio/src/instruction/approve.rs b/sdk-libs/token-pinocchio/src/instruction/approve.rs index af6f821271..21f1e03eaf 100644 --- a/sdk-libs/token-pinocchio/src/instruction/approve.rs +++ b/sdk-libs/token-pinocchio/src/instruction/approve.rs @@ -23,6 +23,7 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// owner: &ctx.accounts.owner, /// system_program: &ctx.accounts.system_program, /// amount: 100, +/// max_top_up: None, /// } /// .invoke()?; /// ``` @@ -32,6 +33,7 @@ pub struct ApproveCpi<'info> { pub owner: &'info AccountInfo, pub system_program: &'info AccountInfo, pub amount: u64, + pub max_top_up: Option, } impl<'info> ApproveCpi<'info> { @@ -40,24 +42,35 @@ impl<'info> ApproveCpi<'info> { } pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { - // Build instruction data: discriminator(1) + amount(8) - let mut data = [0u8; 9]; + // Build instruction data: discriminator(1) + amount(8) + optional max_top_up(2) + let mut data = [0u8; 11]; data[0] = 4u8; // Approve discriminator data[1..9].copy_from_slice(&self.amount.to_le_bytes()); + let mut data_len = 9; + if let Some(max_top_up) = self.max_top_up { + data[9..11].copy_from_slice(&max_top_up.to_le_bytes()); + data_len = 11; + } + + let owner_meta = if self.max_top_up.is_some() { + AccountMeta::writable_signer(self.owner.key()) + } else { + AccountMeta::readonly_signer(self.owner.key()) + }; let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let account_metas = [ AccountMeta::writable(self.token_account.key()), AccountMeta::readonly(self.delegate.key()), - AccountMeta::writable_signer(self.owner.key()), + owner_meta, AccountMeta::readonly(self.system_program.key()), ]; let instruction = Instruction { program_id: &program_id, accounts: &account_metas, - data: &data, + data: &data[..data_len], }; let account_infos = [ diff --git a/sdk-libs/token-pinocchio/src/instruction/revoke.rs b/sdk-libs/token-pinocchio/src/instruction/revoke.rs index 1cbca61334..d981f5a3ed 100644 --- a/sdk-libs/token-pinocchio/src/instruction/revoke.rs +++ b/sdk-libs/token-pinocchio/src/instruction/revoke.rs @@ -21,6 +21,7 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// token_account: &ctx.accounts.token_account, /// owner: &ctx.accounts.owner, /// system_program: &ctx.accounts.system_program, +/// max_top_up: None, /// } /// .invoke()?; /// ``` @@ -28,6 +29,7 @@ pub struct RevokeCpi<'info> { pub token_account: &'info AccountInfo, pub owner: &'info AccountInfo, pub system_program: &'info AccountInfo, + pub max_top_up: Option, } impl<'info> RevokeCpi<'info> { @@ -36,21 +38,33 @@ impl<'info> RevokeCpi<'info> { } pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { - // Build instruction data: discriminator(1) only - let data = [5u8]; // Revoke discriminator + // Build instruction data: discriminator(1) + optional max_top_up(2) + let mut data = [0u8; 3]; + data[0] = 5u8; // Revoke discriminator + let mut data_len = 1; + if let Some(max_top_up) = self.max_top_up { + data[1..3].copy_from_slice(&max_top_up.to_le_bytes()); + data_len = 3; + } + + let owner_meta = if self.max_top_up.is_some() { + AccountMeta::writable_signer(self.owner.key()) + } else { + AccountMeta::readonly_signer(self.owner.key()) + }; let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let account_metas = [ AccountMeta::writable(self.token_account.key()), - AccountMeta::writable_signer(self.owner.key()), + owner_meta, AccountMeta::readonly(self.system_program.key()), ]; let instruction = Instruction { program_id: &program_id, accounts: &account_metas, - data: &data, + data: &data[..data_len], }; let account_infos = [self.token_account, self.owner, self.system_program]; diff --git a/sdk-libs/token-sdk/src/instruction/approve.rs b/sdk-libs/token-sdk/src/instruction/approve.rs index b1c6a8cff1..0f0670b0ec 100644 --- a/sdk-libs/token-sdk/src/instruction/approve.rs +++ b/sdk-libs/token-sdk/src/instruction/approve.rs @@ -17,6 +17,7 @@ use solana_pubkey::Pubkey; /// delegate, /// owner, /// amount: 100, +/// max_top_up: None, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` @@ -29,6 +30,9 @@ pub struct Approve { pub owner: Pubkey, /// Amount of tokens to delegate pub amount: u64, + /// Maximum lamports for compressible top-up. Transaction fails if exceeded. + /// When set, includes max_top_up in instruction data and marks owner writable. + pub max_top_up: Option, } /// # Approve Light Token via CPI: @@ -45,6 +49,7 @@ pub struct Approve { /// owner, /// system_program, /// amount: 100, +/// max_top_up: None, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -55,6 +60,7 @@ pub struct ApproveCpi<'info> { pub owner: AccountInfo<'info>, pub system_program: AccountInfo<'info>, pub amount: u64, + pub max_top_up: Option, } impl<'info> ApproveCpi<'info> { @@ -92,6 +98,7 @@ impl<'info> From<&ApproveCpi<'info>> for Approve { delegate: *cpi.delegate.key, owner: *cpi.owner.key, amount: cpi.amount, + max_top_up: cpi.max_top_up, } } } @@ -100,13 +107,22 @@ impl Approve { pub fn instruction(self) -> Result { let mut data = vec![4u8]; // CTokenApprove discriminator data.extend_from_slice(&self.amount.to_le_bytes()); + if let Some(max_top_up) = self.max_top_up { + data.extend_from_slice(&max_top_up.to_le_bytes()); + } + + let owner_meta = if self.max_top_up.is_some() { + AccountMeta::new(self.owner, true) + } else { + AccountMeta::new_readonly(self.owner, true) + }; Ok(Instruction { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), accounts: vec![ AccountMeta::new(self.token_account, false), AccountMeta::new_readonly(self.delegate, false), - AccountMeta::new(self.owner, true), + owner_meta, AccountMeta::new_readonly(Pubkey::default(), false), ], data, diff --git a/sdk-libs/token-sdk/src/instruction/close.rs b/sdk-libs/token-sdk/src/instruction/close.rs index 361e399302..c3c9f68a2d 100644 --- a/sdk-libs/token-sdk/src/instruction/close.rs +++ b/sdk-libs/token-sdk/src/instruction/close.rs @@ -49,7 +49,7 @@ impl CloseAccount { let accounts = vec![ AccountMeta::new(self.account, false), AccountMeta::new(self.destination, false), - AccountMeta::new(self.owner, true), // signer, mutable to receive write_top_up + AccountMeta::new_readonly(self.owner, true), AccountMeta::new(self.rent_sponsor, false), ]; diff --git a/sdk-libs/token-sdk/src/instruction/revoke.rs b/sdk-libs/token-sdk/src/instruction/revoke.rs index d746dbe850..6fa8a4b280 100644 --- a/sdk-libs/token-sdk/src/instruction/revoke.rs +++ b/sdk-libs/token-sdk/src/instruction/revoke.rs @@ -14,6 +14,7 @@ use solana_pubkey::Pubkey; /// let instruction = Revoke { /// token_account, /// owner, +/// max_top_up: None, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` @@ -22,6 +23,9 @@ pub struct Revoke { pub token_account: Pubkey, /// Owner of the Light Token account (signer, payer for top-up) pub owner: Pubkey, + /// Maximum lamports for compressible top-up. Transaction fails if exceeded. + /// When set, includes max_top_up in instruction data and marks owner writable. + pub max_top_up: Option, } /// # Revoke Light Token via CPI: @@ -35,6 +39,7 @@ pub struct Revoke { /// token_account, /// owner, /// system_program, +/// max_top_up: None, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -43,6 +48,7 @@ pub struct RevokeCpi<'info> { pub token_account: AccountInfo<'info>, pub owner: AccountInfo<'info>, pub system_program: AccountInfo<'info>, + pub max_top_up: Option, } impl<'info> RevokeCpi<'info> { @@ -68,20 +74,32 @@ impl<'info> From<&RevokeCpi<'info>> for Revoke { Self { token_account: *cpi.token_account.key, owner: *cpi.owner.key, + max_top_up: cpi.max_top_up, } } } impl Revoke { pub fn instruction(self) -> Result { + let mut data = vec![5u8]; // CTokenRevoke discriminator + if let Some(max_top_up) = self.max_top_up { + data.extend_from_slice(&max_top_up.to_le_bytes()); + } + + let owner_meta = if self.max_top_up.is_some() { + AccountMeta::new(self.owner, true) + } else { + AccountMeta::new_readonly(self.owner, true) + }; + Ok(Instruction { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), accounts: vec![ AccountMeta::new(self.token_account, false), - AccountMeta::new(self.owner, true), + owner_meta, AccountMeta::new_readonly(Pubkey::default(), false), ], - data: vec![5u8], // CTokenRevoke discriminator + data, }) } } diff --git a/sdk-libs/token-sdk/tests/instruction_close.rs b/sdk-libs/token-sdk/tests/instruction_close.rs index 569f4b035e..c0f13a830d 100644 --- a/sdk-libs/token-sdk/tests/instruction_close.rs +++ b/sdk-libs/token-sdk/tests/instruction_close.rs @@ -21,7 +21,7 @@ fn test_close_account_instruction() { accounts: vec![ AccountMeta::new(account, false), // account: writable, not signer AccountMeta::new(destination, false), // destination: writable, not signer - AccountMeta::new(owner, true), // owner: writable, signer + AccountMeta::new_readonly(owner, true), // owner: readonly, signer AccountMeta::new(LIGHT_TOKEN_RENT_SPONSOR, false), // rent_sponsor: writable, not signer ], data: vec![9u8], // CloseAccount discriminator @@ -54,7 +54,7 @@ fn test_close_account_custom_rent_sponsor() { accounts: vec![ AccountMeta::new(account, false), // account: writable, not signer AccountMeta::new(destination, false), // destination: writable, not signer - AccountMeta::new(owner, true), // owner: writable, signer + AccountMeta::new_readonly(owner, true), // owner: readonly, signer AccountMeta::new(custom_sponsor, false), // custom_sponsor: writable, not signer ], data: vec![9u8], // CloseAccount discriminator diff --git a/sdk-tests/sdk-light-token-pinocchio/src/approve.rs b/sdk-tests/sdk-light-token-pinocchio/src/approve.rs index 3aacf4d3ed..1950a65815 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/approve.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/approve.rs @@ -36,6 +36,7 @@ pub fn process_approve_invoke( owner: &accounts[2], system_program: &accounts[3], amount: data.amount, + max_top_up: None, } .invoke()?; @@ -74,6 +75,7 @@ pub fn process_approve_invoke_signed( owner: &accounts[2], system_program: &accounts[3], amount: data.amount, + max_top_up: None, } .invoke_signed(&[signer])?; diff --git a/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs b/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs index a1fb1508f9..8488de95de 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs @@ -23,6 +23,7 @@ pub fn process_revoke_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramErro token_account: &accounts[0], owner: &accounts[1], system_program: &accounts[2], + max_top_up: None, } .invoke()?; @@ -57,6 +58,7 @@ pub fn process_revoke_invoke_signed(accounts: &[AccountInfo]) -> Result<(), Prog token_account: &accounts[0], owner: &accounts[1], system_program: &accounts[2], + max_top_up: None, } .invoke_signed(&[signer])?; diff --git a/sdk-tests/sdk-light-token-test/src/approve.rs b/sdk-tests/sdk-light-token-test/src/approve.rs index 0dda096b88..e9b4f14189 100644 --- a/sdk-tests/sdk-light-token-test/src/approve.rs +++ b/sdk-tests/sdk-light-token-test/src/approve.rs @@ -32,6 +32,7 @@ pub fn process_approve_invoke( owner: accounts[2].clone(), system_program: accounts[3].clone(), amount: data.amount, + max_top_up: None, } .invoke()?; @@ -69,6 +70,7 @@ pub fn process_approve_invoke_signed( owner: accounts[2].clone(), system_program: accounts[3].clone(), amount: data.amount, + max_top_up: None, } .invoke_signed(&[signer_seeds])?; diff --git a/sdk-tests/sdk-light-token-test/src/revoke.rs b/sdk-tests/sdk-light-token-test/src/revoke.rs index ff55fbecce..99fb65b27d 100644 --- a/sdk-tests/sdk-light-token-test/src/revoke.rs +++ b/sdk-tests/sdk-light-token-test/src/revoke.rs @@ -19,6 +19,7 @@ pub fn process_revoke_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramErro token_account: accounts[0].clone(), owner: accounts[1].clone(), system_program: accounts[2].clone(), + max_top_up: None, } .invoke()?; @@ -50,6 +51,7 @@ pub fn process_revoke_invoke_signed(accounts: &[AccountInfo]) -> Result<(), Prog token_account: accounts[0].clone(), owner: accounts[1].clone(), system_program: accounts[2].clone(), + max_top_up: None, } .invoke_signed(&[signer_seeds])?; From f82717eb8bfa0621a16a8944e865ef7d92f6e304 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Thu, 19 Feb 2026 12:33:48 +0000 Subject: [PATCH 02/26] fix(token-sdk): return ProgramError from get_token_account_balance Delegate to Token::amount_from_account_info and return ProgramError instead of TokenSdkError so callers can use ? directly in Anchor contexts without redundant .map_err(). --- sdk-libs/token-sdk/Cargo.toml | 2 +- sdk-libs/token-sdk/src/utils.rs | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/sdk-libs/token-sdk/Cargo.toml b/sdk-libs/token-sdk/Cargo.toml index d01c4ca629..664d020372 100644 --- a/sdk-libs/token-sdk/Cargo.toml +++ b/sdk-libs/token-sdk/Cargo.toml @@ -33,7 +33,7 @@ light-compressed-token-sdk = { workspace = true } light-token-types = { workspace = true } light-compressed-account = { workspace = true, features = ["std", "solana"] } light-compressible = { workspace = true } -light-token-interface = { workspace = true } +light-token-interface = { workspace = true, features = ["solana"] } light-sdk = { workspace = true, features = ["v2", "cpi-context"] } light-account = { workspace = true } light-batched-merkle-tree = { workspace = true } diff --git a/sdk-libs/token-sdk/src/utils.rs b/sdk-libs/token-sdk/src/utils.rs index 1df6ad6cde..f589f484c9 100644 --- a/sdk-libs/token-sdk/src/utils.rs +++ b/sdk-libs/token-sdk/src/utils.rs @@ -4,6 +4,7 @@ pub use light_compressed_token_sdk::utils::TokenDefaultAccounts; use light_token_interface::state::Token; use solana_account_info::AccountInfo; +use solana_program_error::ProgramError; use solana_pubkey::Pubkey; use crate::{constants::LIGHT_TOKEN_PROGRAM_ID as PROGRAM_ID, error::TokenSdkError}; @@ -22,11 +23,8 @@ pub fn get_associated_token_address_and_bump(owner: &Pubkey, mint: &Pubkey) -> ( } /// Get the token balance from a Light token account. -pub fn get_token_account_balance(token_account_info: &AccountInfo) -> Result { - let data = token_account_info - .try_borrow_data() - .map_err(|_| TokenSdkError::AccountBorrowFailed)?; - Token::amount_from_slice(&data).map_err(|_| TokenSdkError::InvalidAccountData) +pub fn get_token_account_balance(token_account_info: &AccountInfo) -> Result { + Token::amount_from_account_info(token_account_info).map_err(Into::into) } /// Check if an account owner is a Light token program. From 6187e991aa136af6fc7a92794d3d75dbe90cf8d2 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Thu, 19 Feb 2026 16:05:55 +0000 Subject: [PATCH 03/26] fix(tests): add max_top_up field to Approve and Revoke in compressed-token-test --- .../tests/light_token/approve_revoke.rs | 2 ++ .../tests/light_token/shared.rs | 20 ++++++------------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs b/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs index cd1bb64d41..aced597069 100644 --- a/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs +++ b/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs @@ -436,6 +436,7 @@ async fn test_approve_revoke_compressible() -> Result<(), RpcError> { delegate: delegate.pubkey(), owner: owner.pubkey(), amount: approve_amount, + max_top_up: None, } .instruction() .map_err(|e| { @@ -460,6 +461,7 @@ async fn test_approve_revoke_compressible() -> Result<(), RpcError> { let revoke_ix = Revoke { token_account: account_pubkey, owner: owner.pubkey(), + max_top_up: None, } .instruction() .map_err(|e| RpcError::AssertRpcError(format!("Failed to create revoke instruction: {}", e)))?; diff --git a/program-tests/compressed-token-test/tests/light_token/shared.rs b/program-tests/compressed-token-test/tests/light_token/shared.rs index f1d65ad313..d57af2d6a6 100644 --- a/program-tests/compressed-token-test/tests/light_token/shared.rs +++ b/program-tests/compressed-token-test/tests/light_token/shared.rs @@ -903,6 +903,7 @@ pub async fn approve_and_assert( delegate, owner: context.owner_keypair.pubkey(), amount, + max_top_up: None, } .instruction() .unwrap(); @@ -941,21 +942,16 @@ pub async fn approve_and_assert_fails( ) { println!("Approve (expecting failure) initiated for: {}", name); - // Build using SDK, then modify if needed for max_top_up - let mut instruction = Approve { + let instruction = Approve { token_account, delegate, owner: authority.pubkey(), amount, + max_top_up, } .instruction() .unwrap(); - // Add max_top_up to instruction data if specified - if let Some(max) = max_top_up { - instruction.data.extend_from_slice(&max.to_le_bytes()); - } - let result = context .rpc .create_and_send_transaction( @@ -976,6 +972,7 @@ pub async fn revoke_and_assert(context: &mut AccountTestContext, name: &str) { let revoke_ix = Revoke { token_account: context.token_account_keypair.pubkey(), owner: context.owner_keypair.pubkey(), + max_top_up: None, } .instruction() .unwrap(); @@ -1005,19 +1002,14 @@ pub async fn revoke_and_assert_fails( ) { println!("Revoke (expecting failure) initiated for: {}", name); - // Build using SDK, then modify if needed for max_top_up - let mut instruction = Revoke { + let instruction = Revoke { token_account, owner: authority.pubkey(), + max_top_up, } .instruction() .unwrap(); - // Add max_top_up to instruction data if specified - if let Some(max) = max_top_up { - instruction.data.extend_from_slice(&max.to_le_bytes()); - } - let result = context .rpc .create_and_send_transaction( From d49cc7d5aa0d98fdf114df3fe7e6e72aa9a0cddf Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Thu, 19 Feb 2026 17:08:05 +0000 Subject: [PATCH 04/26] before reverting pro --- .../tests/light_token/approve_revoke.rs | 72 +- .../tests/light_token/burn.rs | 10 - .../tests/light_token/extensions_failing.rs | 3 - .../tests/light_token/shared.rs | 10 +- .../tests/light_token/transfer.rs | 38 +- .../tests/light_token/transfer_checked.rs | 2 - .../compressed-token-test/tests/mint/burn.rs | 2 - .../tests/mint/mint_to.rs | 4 - .../registry-test/tests/compressible.rs | 1 - sdk-libs/token-client/src/actions/approve.rs | 4 +- sdk-libs/token-client/src/actions/mint_to.rs | 1 - sdk-libs/token-client/src/actions/revoke.rs | 4 +- sdk-libs/token-client/src/actions/transfer.rs | 1 - .../src/actions/transfer_checked.rs | 1 - .../src/actions/transfer_interface.rs | 1 - .../src/instruction/approve.rs | 89 ++- .../token-pinocchio/src/instruction/revoke.rs | 75 +- sdk-libs/token-sdk/src/instruction/approve.rs | 91 ++- sdk-libs/token-sdk/src/instruction/burn.rs | 18 +- .../token-sdk/src/instruction/burn_checked.rs | 18 +- sdk-libs/token-sdk/src/instruction/mint_to.rs | 18 +- .../src/instruction/mint_to_checked.rs | 18 +- sdk-libs/token-sdk/src/instruction/revoke.rs | 57 +- .../token-sdk/src/instruction/transfer.rs | 18 +- .../src/instruction/transfer_checked.rs | 19 +- .../src/instruction/transfer_interface.rs | 5 - .../target/idl/sdk_anchor_test.json | 726 ----------------- .../target/types/sdk_anchor_test.ts | 732 ------------------ .../sdk-light-token-pinocchio/src/approve.rs | 4 +- .../sdk-light-token-pinocchio/src/revoke.rs | 4 +- sdk-tests/sdk-light-token-test/src/approve.rs | 4 +- sdk-tests/sdk-light-token-test/src/burn.rs | 2 - .../src/ctoken_mint_to.rs | 2 - sdk-tests/sdk-light-token-test/src/revoke.rs | 4 +- .../sdk-light-token-test/src/transfer.rs | 2 - .../src/transfer_checked.rs | 2 - .../tests/scenario_light_mint.rs | 1 - .../scenario_light_mint_compression_only.rs | 1 - .../sdk-light-token-test/tests/shared.rs | 3 - 39 files changed, 257 insertions(+), 1810 deletions(-) delete mode 100644 sdk-tests/sdk-anchor-test/target/idl/sdk_anchor_test.json delete mode 100644 sdk-tests/sdk-anchor-test/target/types/sdk_anchor_test.ts diff --git a/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs b/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs index aced597069..f3270fea0a 100644 --- a/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs +++ b/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs @@ -10,7 +10,6 @@ //! | Invalid ctoken (non-existent) | test_approve_fails | test_revoke_fails | //! | Invalid ctoken (wrong owner) | test_approve_fails | test_revoke_fails | //! | Invalid ctoken (spl account) | test_approve_fails | test_revoke_fails | -//! | Max top-up exceeded | test_approve_fails | test_revoke_fails | //! //! **Note**: "Invalid mint" tests not applicable - approve/revoke don't take mint as account. @@ -81,7 +80,6 @@ async fn test_approve_fails() { delegate.pubkey(), &owner, 100, - None, "non_existent_account", 6153, // NotRentExempt (SPL Token code 0 -> ErrorCode::NotRentExempt) ) @@ -129,44 +127,11 @@ async fn test_approve_fails() { delegate.pubkey(), &owner, 100, - None, "wrong_program_owner", 13, // InstructionError::ExternalAccountDataModified - program tried to modify account it doesn't own ) .await; } - - // Test 3: Max top-up exceeded - { - let mut context = setup_account_test_with_created_account(Some((10, false))) - .await - .unwrap(); - - // Fund owner so the test doesn't fail due to insufficient lamports - context - .rpc - .airdrop_lamports(&context.owner_keypair.pubkey(), 1_000_000_000) - .await - .unwrap(); - - // Warp time to trigger top-up requirement (past funded epochs) - context.rpc.warp_to_slot(SLOTS_PER_EPOCH * 12 + 1).unwrap(); - - let delegate = Keypair::new(); - let token_account = context.token_account_keypair.pubkey(); - let owner = context.owner_keypair.insecure_clone(); - approve_and_assert_fails( - &mut context, - token_account, - delegate.pubkey(), - &owner, - 100, - Some(1), // max_top_up = 1 (1,000 lamports budget, still too low for rent top-up) - "max_topup_exceeded", - 18043, // TokenError::MaxTopUpExceeded - ) - .await; - } } // ============================================================================ @@ -251,7 +216,6 @@ async fn test_revoke_fails() { &mut context, non_existent, &owner, - None, "non_existent_account", 6153, // NotRentExempt (SPL Token code 0 -> ErrorCode::NotRentExempt) ) @@ -296,43 +260,11 @@ async fn test_revoke_fails() { &mut context, wrong_owner_account.pubkey(), &owner, - None, "wrong_program_owner", 13, // InstructionError::ExternalAccountDataModified - program tried to modify account it doesn't own ) .await; } - - // Test 3: Max top-up exceeded - { - let mut context = setup_account_test_with_created_account(Some((10, false))) - .await - .unwrap(); - - // First approve to set delegate (need to do before warping) - context - .rpc - .airdrop_lamports(&context.owner_keypair.pubkey(), 1_000_000_000) - .await - .unwrap(); - let delegate = Keypair::new(); - approve_and_assert(&mut context, delegate.pubkey(), 100, "approve_before_warp").await; - - // Warp time to trigger top-up requirement (past funded epochs) - context.rpc.warp_to_slot(SLOTS_PER_EPOCH * 12 + 1).unwrap(); - - let token_account = context.token_account_keypair.pubkey(); - let owner = context.owner_keypair.insecure_clone(); - revoke_and_assert_fails( - &mut context, - token_account, - &owner, - Some(1), // max_top_up = 1 (1,000 lamports budget, still too low for rent top-up) - "max_topup_exceeded", - 18043, // TokenError::MaxTopUpExceeded - ) - .await; - } } // ============================================================================ @@ -436,7 +368,7 @@ async fn test_approve_revoke_compressible() -> Result<(), RpcError> { delegate: delegate.pubkey(), owner: owner.pubkey(), amount: approve_amount, - max_top_up: None, + fee_payer: None, } .instruction() .map_err(|e| { @@ -461,7 +393,7 @@ async fn test_approve_revoke_compressible() -> Result<(), RpcError> { let revoke_ix = Revoke { token_account: account_pubkey, owner: owner.pubkey(), - max_top_up: None, + fee_payer: None, } .instruction() .map_err(|e| RpcError::AssertRpcError(format!("Failed to create revoke instruction: {}", e)))?; diff --git a/program-tests/compressed-token-test/tests/light_token/burn.rs b/program-tests/compressed-token-test/tests/light_token/burn.rs index 4ad447582d..23db86b1a4 100644 --- a/program-tests/compressed-token-test/tests/light_token/burn.rs +++ b/program-tests/compressed-token-test/tests/light_token/burn.rs @@ -47,7 +47,6 @@ async fn test_burn_success_cases() { mint: ctx.mint_pda, amount: burn_amount, authority: ctx.owner_keypair.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() @@ -79,7 +78,6 @@ async fn test_burn_success_cases() { mint: ctx.mint_pda, amount: burn_amount, authority: ctx.owner_keypair.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() @@ -130,7 +128,6 @@ async fn test_burn_fails() { mint: other_mint_pda, // Wrong mint amount: 50, authority: ctx.owner_keypair.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() @@ -161,7 +158,6 @@ async fn test_burn_fails() { mint: ctx.mint_pda, amount: 50, authority: ctx.owner_keypair.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() @@ -208,7 +204,6 @@ async fn test_burn_fails() { mint: ctx.mint_pda, amount: 50, authority: ctx.owner_keypair.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() @@ -239,7 +234,6 @@ async fn test_burn_fails() { mint: ctx.mint_pda, amount: 200, // More than 100 balance authority: ctx.owner_keypair.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() @@ -275,7 +269,6 @@ async fn test_burn_fails() { mint: ctx.mint_pda, amount: 50, authority: wrong_authority.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() @@ -379,7 +372,6 @@ async fn setup_burn_test() -> BurnTestContext { destination: ctoken_ata, amount: 100, authority: mint_authority.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() @@ -425,7 +417,6 @@ async fn test_burn_checked_success() { amount: burn_amount, decimals: 8, // Correct decimals authority: ctx.owner_keypair.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() @@ -458,7 +449,6 @@ async fn test_burn_checked_wrong_decimals() { amount: 50, decimals: 7, // Wrong decimals authority: ctx.owner_keypair.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() diff --git a/program-tests/compressed-token-test/tests/light_token/extensions_failing.rs b/program-tests/compressed-token-test/tests/light_token/extensions_failing.rs index 7b70820bc4..ae48e7b852 100644 --- a/program-tests/compressed-token-test/tests/light_token/extensions_failing.rs +++ b/program-tests/compressed-token-test/tests/light_token/extensions_failing.rs @@ -195,7 +195,6 @@ async fn test_ctoken_transfer_fails_when_mint_paused() { amount: 100_000_000, decimals: 9, authority: owner.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() @@ -243,7 +242,6 @@ async fn test_ctoken_transfer_fails_with_non_zero_transfer_fee() { amount: 100_000_000, decimals: 9, authority: owner.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() @@ -292,7 +290,6 @@ async fn test_ctoken_transfer_fails_with_non_nil_transfer_hook() { amount: 100_000_000, decimals: 9, authority: owner.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() diff --git a/program-tests/compressed-token-test/tests/light_token/shared.rs b/program-tests/compressed-token-test/tests/light_token/shared.rs index d57af2d6a6..3e5743105a 100644 --- a/program-tests/compressed-token-test/tests/light_token/shared.rs +++ b/program-tests/compressed-token-test/tests/light_token/shared.rs @@ -903,7 +903,7 @@ pub async fn approve_and_assert( delegate, owner: context.owner_keypair.pubkey(), amount, - max_top_up: None, + fee_payer: None, } .instruction() .unwrap(); @@ -936,7 +936,6 @@ pub async fn approve_and_assert_fails( delegate: Pubkey, authority: &Keypair, amount: u64, - max_top_up: Option, name: &str, expected_error_code: u32, ) { @@ -947,7 +946,7 @@ pub async fn approve_and_assert_fails( delegate, owner: authority.pubkey(), amount, - max_top_up, + fee_payer: None, } .instruction() .unwrap(); @@ -972,7 +971,7 @@ pub async fn revoke_and_assert(context: &mut AccountTestContext, name: &str) { let revoke_ix = Revoke { token_account: context.token_account_keypair.pubkey(), owner: context.owner_keypair.pubkey(), - max_top_up: None, + fee_payer: None, } .instruction() .unwrap(); @@ -996,7 +995,6 @@ pub async fn revoke_and_assert_fails( context: &mut AccountTestContext, token_account: Pubkey, authority: &Keypair, - max_top_up: Option, name: &str, expected_error_code: u32, ) { @@ -1005,7 +1003,7 @@ pub async fn revoke_and_assert_fails( let instruction = Revoke { token_account, owner: authority.pubkey(), - max_top_up, + fee_payer: None, } .instruction() .unwrap(); diff --git a/program-tests/compressed-token-test/tests/light_token/transfer.rs b/program-tests/compressed-token-test/tests/light_token/transfer.rs index a5346b9d14..0971f88cd5 100644 --- a/program-tests/compressed-token-test/tests/light_token/transfer.rs +++ b/program-tests/compressed-token-test/tests/light_token/transfer.rs @@ -860,7 +860,6 @@ async fn transfer_checked_and_assert( amount, decimals, authority: authority.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() @@ -902,7 +901,6 @@ async fn transfer_checked_and_assert_fails( amount, decimals, authority: authority.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() @@ -1068,18 +1066,30 @@ async fn test_ctoken_transfer_checked_max_top_up_exceeded() { let owner_keypair = context.owner_keypair.insecure_clone(); let payer_pubkey = context.payer.pubkey(); - let transfer_ix = TransferChecked { - source, - mint, - destination, - amount: 100, - decimals: 9, - authority: owner_keypair.pubkey(), - max_top_up: Some(1), - fee_payer: None, - } - .instruction() - .unwrap(); + // Build raw instruction with low max_top_up to test on-chain error path + // (TransferChecked struct no longer exposes max_top_up) + let transfer_ix = { + use anchor_lang::prelude::AccountMeta; + use solana_sdk::instruction::Instruction; + + // Discriminator (12) + amount (8) + decimals (1) + max_top_up (2) + let mut data = vec![12u8]; + data.extend_from_slice(&100u64.to_le_bytes()); + data.push(9u8); // decimals + data.extend_from_slice(&1u16.to_le_bytes()); // max_top_up = 1 + + Instruction { + program_id: light_compressed_token::ID, + accounts: vec![ + AccountMeta::new(source, false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(destination, false), + AccountMeta::new(owner_keypair.pubkey(), true), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ], + data, + } + }; let result = context .rpc diff --git a/program-tests/compressed-token-test/tests/light_token/transfer_checked.rs b/program-tests/compressed-token-test/tests/light_token/transfer_checked.rs index ed3be69cb3..f91a0f18a9 100644 --- a/program-tests/compressed-token-test/tests/light_token/transfer_checked.rs +++ b/program-tests/compressed-token-test/tests/light_token/transfer_checked.rs @@ -162,7 +162,6 @@ async fn test_transfer_requires_checked_for_restricted_extensions() { destination: account_b_pubkey, amount: transfer_amount, authority: owner.pubkey(), - max_top_up: Some(u16::MAX), // u16::MAX = no limit, includes system program for compressible fee_payer: None, } .instruction() @@ -186,7 +185,6 @@ async fn test_transfer_requires_checked_for_restricted_extensions() { amount: transfer_amount, decimals: 9, authority: owner.pubkey(), - max_top_up: Some(u16::MAX), // u16::MAX = no limit, includes system program for compressible fee_payer: None, } .instruction() diff --git a/program-tests/compressed-token-test/tests/mint/burn.rs b/program-tests/compressed-token-test/tests/mint/burn.rs index 37a73a9447..42c279df99 100644 --- a/program-tests/compressed-token-test/tests/mint/burn.rs +++ b/program-tests/compressed-token-test/tests/mint/burn.rs @@ -118,7 +118,6 @@ async fn test_ctoken_burn() { mint: ctx.mint_pda, amount: 500, authority: ctx.owner_keypair.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() @@ -141,7 +140,6 @@ async fn test_ctoken_burn() { mint: ctx.mint_pda, amount: 500, authority: ctx.owner_keypair.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() diff --git a/program-tests/compressed-token-test/tests/mint/mint_to.rs b/program-tests/compressed-token-test/tests/mint/mint_to.rs index 88efbc2afd..fa469af6df 100644 --- a/program-tests/compressed-token-test/tests/mint/mint_to.rs +++ b/program-tests/compressed-token-test/tests/mint/mint_to.rs @@ -96,7 +96,6 @@ async fn test_ctoken_mint_to() { destination: ctx.ctoken_account, amount: 500, authority: ctx.mint_authority.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() @@ -119,7 +118,6 @@ async fn test_ctoken_mint_to() { destination: ctx.ctoken_account, amount: 500, authority: ctx.mint_authority.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() @@ -171,7 +169,6 @@ async fn test_ctoken_mint_to_checked_success() { amount: 500, decimals: 8, // Correct decimals authority: ctx.mint_authority.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() @@ -214,7 +211,6 @@ async fn test_ctoken_mint_to_checked_wrong_decimals() { amount: 500, decimals: 7, // Wrong decimals authority: ctx.mint_authority.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() diff --git a/program-tests/registry-test/tests/compressible.rs b/program-tests/registry-test/tests/compressible.rs index 645e40c468..76daa90b74 100644 --- a/program-tests/registry-test/tests/compressible.rs +++ b/program-tests/registry-test/tests/compressible.rs @@ -1253,7 +1253,6 @@ async fn mint_to_token( destination, amount, authority: mint_authority.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() diff --git a/sdk-libs/token-client/src/actions/approve.rs b/sdk-libs/token-client/src/actions/approve.rs index b86ae80ef7..1bf7d4b938 100644 --- a/sdk-libs/token-client/src/actions/approve.rs +++ b/sdk-libs/token-client/src/actions/approve.rs @@ -79,7 +79,7 @@ impl Approve { delegate: self.delegate, owner: owner_pubkey, amount: self.amount, - max_top_up: None, + fee_payer: None, } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; @@ -122,7 +122,7 @@ impl Approve { delegate: self.delegate, owner: owner.pubkey(), amount: self.amount, - max_top_up: None, + fee_payer: None, } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; diff --git a/sdk-libs/token-client/src/actions/mint_to.rs b/sdk-libs/token-client/src/actions/mint_to.rs index af7d76403f..38b3146831 100644 --- a/sdk-libs/token-client/src/actions/mint_to.rs +++ b/sdk-libs/token-client/src/actions/mint_to.rs @@ -58,7 +58,6 @@ impl MintTo { destination: self.destination, amount: self.amount, authority: authority.pubkey(), - max_top_up: None, fee_payer, } .instruction() diff --git a/sdk-libs/token-client/src/actions/revoke.rs b/sdk-libs/token-client/src/actions/revoke.rs index cab0408e78..6e7a20b1f4 100644 --- a/sdk-libs/token-client/src/actions/revoke.rs +++ b/sdk-libs/token-client/src/actions/revoke.rs @@ -69,7 +69,7 @@ impl Revoke { let ix = RevokeInstruction { token_account: self.token_account, owner: owner_pubkey, - max_top_up: None, + fee_payer: None, } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; @@ -96,7 +96,7 @@ impl Revoke { let ix = RevokeInstruction { token_account: self.token_account, owner: owner.pubkey(), - max_top_up: None, + fee_payer: None, } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; diff --git a/sdk-libs/token-client/src/actions/transfer.rs b/sdk-libs/token-client/src/actions/transfer.rs index de6089df55..c41f867d58 100644 --- a/sdk-libs/token-client/src/actions/transfer.rs +++ b/sdk-libs/token-client/src/actions/transfer.rs @@ -58,7 +58,6 @@ impl Transfer { destination: self.destination, amount: self.amount, authority: authority.pubkey(), - max_top_up: None, fee_payer, } .instruction() diff --git a/sdk-libs/token-client/src/actions/transfer_checked.rs b/sdk-libs/token-client/src/actions/transfer_checked.rs index 0a5e0f3d7b..8480336417 100644 --- a/sdk-libs/token-client/src/actions/transfer_checked.rs +++ b/sdk-libs/token-client/src/actions/transfer_checked.rs @@ -69,7 +69,6 @@ impl TransferChecked { amount: self.amount, decimals: self.decimals, authority: authority.pubkey(), - max_top_up: None, fee_payer, } .instruction() diff --git a/sdk-libs/token-client/src/actions/transfer_interface.rs b/sdk-libs/token-client/src/actions/transfer_interface.rs index 9ecaf9ca1b..741d65d599 100644 --- a/sdk-libs/token-client/src/actions/transfer_interface.rs +++ b/sdk-libs/token-client/src/actions/transfer_interface.rs @@ -104,7 +104,6 @@ impl TransferInterface { authority: authority.pubkey(), payer: payer.pubkey(), spl_interface, - max_top_up: None, source_owner, destination_owner, } diff --git a/sdk-libs/token-pinocchio/src/instruction/approve.rs b/sdk-libs/token-pinocchio/src/instruction/approve.rs index 21f1e03eaf..999096778f 100644 --- a/sdk-libs/token-pinocchio/src/instruction/approve.rs +++ b/sdk-libs/token-pinocchio/src/instruction/approve.rs @@ -23,7 +23,7 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// owner: &ctx.accounts.owner, /// system_program: &ctx.accounts.system_program, /// amount: 100, -/// max_top_up: None, +/// fee_payer: None, /// } /// .invoke()?; /// ``` @@ -33,7 +33,8 @@ pub struct ApproveCpi<'info> { pub owner: &'info AccountInfo, pub system_program: &'info AccountInfo, pub amount: u64, - pub max_top_up: Option, + /// Optional fee payer for rent top-ups. If not provided, owner pays. + pub fee_payer: Option<&'info AccountInfo>, } impl<'info> ApproveCpi<'info> { @@ -42,48 +43,68 @@ impl<'info> ApproveCpi<'info> { } pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { - // Build instruction data: discriminator(1) + amount(8) + optional max_top_up(2) + // Build instruction data: discriminator(1) + amount(8) + max_top_up(2) let mut data = [0u8; 11]; data[0] = 4u8; // Approve discriminator data[1..9].copy_from_slice(&self.amount.to_le_bytes()); - let mut data_len = 9; - if let Some(max_top_up) = self.max_top_up { - data[9..11].copy_from_slice(&max_top_up.to_le_bytes()); - data_len = 11; - } - - let owner_meta = if self.max_top_up.is_some() { - AccountMeta::writable_signer(self.owner.key()) - } else { - AccountMeta::readonly_signer(self.owner.key()) - }; + data[9..11].copy_from_slice(&u16::MAX.to_le_bytes()); let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); - let account_metas = [ - AccountMeta::writable(self.token_account.key()), - AccountMeta::readonly(self.delegate.key()), - owner_meta, - AccountMeta::readonly(self.system_program.key()), - ]; + if let Some(fee_payer) = self.fee_payer { + let account_metas = [ + AccountMeta::writable(self.token_account.key()), + AccountMeta::readonly(self.delegate.key()), + AccountMeta::readonly_signer(self.owner.key()), + AccountMeta::readonly(self.system_program.key()), + AccountMeta::writable_signer(fee_payer.key()), + ]; - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data[..data_len], - }; + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data, + }; - let account_infos = [ - self.token_account, - self.delegate, - self.owner, - self.system_program, - ]; + let account_infos = [ + self.token_account, + self.delegate, + self.owner, + self.system_program, + fee_payer, + ]; - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } } else { - slice_invoke_signed(&instruction, &account_infos, signers) + let account_metas = [ + AccountMeta::writable(self.token_account.key()), + AccountMeta::readonly(self.delegate.key()), + AccountMeta::writable_signer(self.owner.key()), + AccountMeta::readonly(self.system_program.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data, + }; + + let account_infos = [ + self.token_account, + self.delegate, + self.owner, + self.system_program, + ]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } } } } diff --git a/sdk-libs/token-pinocchio/src/instruction/revoke.rs b/sdk-libs/token-pinocchio/src/instruction/revoke.rs index d981f5a3ed..16faed6127 100644 --- a/sdk-libs/token-pinocchio/src/instruction/revoke.rs +++ b/sdk-libs/token-pinocchio/src/instruction/revoke.rs @@ -21,7 +21,7 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// token_account: &ctx.accounts.token_account, /// owner: &ctx.accounts.owner, /// system_program: &ctx.accounts.system_program, -/// max_top_up: None, +/// fee_payer: None, /// } /// .invoke()?; /// ``` @@ -29,7 +29,8 @@ pub struct RevokeCpi<'info> { pub token_account: &'info AccountInfo, pub owner: &'info AccountInfo, pub system_program: &'info AccountInfo, - pub max_top_up: Option, + /// Optional fee payer for rent top-ups. If not provided, owner pays. + pub fee_payer: Option<&'info AccountInfo>, } impl<'info> RevokeCpi<'info> { @@ -38,41 +39,59 @@ impl<'info> RevokeCpi<'info> { } pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { - // Build instruction data: discriminator(1) + optional max_top_up(2) + // Build instruction data: discriminator(1) + max_top_up(2) let mut data = [0u8; 3]; data[0] = 5u8; // Revoke discriminator - let mut data_len = 1; - if let Some(max_top_up) = self.max_top_up { - data[1..3].copy_from_slice(&max_top_up.to_le_bytes()); - data_len = 3; - } - - let owner_meta = if self.max_top_up.is_some() { - AccountMeta::writable_signer(self.owner.key()) - } else { - AccountMeta::readonly_signer(self.owner.key()) - }; + data[1..3].copy_from_slice(&u16::MAX.to_le_bytes()); let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); - let account_metas = [ - AccountMeta::writable(self.token_account.key()), - owner_meta, - AccountMeta::readonly(self.system_program.key()), - ]; + if let Some(fee_payer) = self.fee_payer { + let account_metas = [ + AccountMeta::writable(self.token_account.key()), + AccountMeta::readonly_signer(self.owner.key()), + AccountMeta::readonly(self.system_program.key()), + AccountMeta::writable_signer(fee_payer.key()), + ]; - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data[..data_len], - }; + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data, + }; - let account_infos = [self.token_account, self.owner, self.system_program]; + let account_infos = [ + self.token_account, + self.owner, + self.system_program, + fee_payer, + ]; - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } } else { - slice_invoke_signed(&instruction, &account_infos, signers) + let account_metas = [ + AccountMeta::writable(self.token_account.key()), + AccountMeta::writable_signer(self.owner.key()), + AccountMeta::readonly(self.system_program.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data, + }; + + let account_infos = [self.token_account, self.owner, self.system_program]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } } } } diff --git a/sdk-libs/token-sdk/src/instruction/approve.rs b/sdk-libs/token-sdk/src/instruction/approve.rs index 0f0670b0ec..11f20c340d 100644 --- a/sdk-libs/token-sdk/src/instruction/approve.rs +++ b/sdk-libs/token-sdk/src/instruction/approve.rs @@ -17,7 +17,7 @@ use solana_pubkey::Pubkey; /// delegate, /// owner, /// amount: 100, -/// max_top_up: None, +/// fee_payer: None, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` @@ -26,13 +26,12 @@ pub struct Approve { pub token_account: Pubkey, /// Delegate to approve pub delegate: Pubkey, - /// Owner of the Light Token account (signer, payer for top-up) + /// Owner of the Light Token account (signer) pub owner: Pubkey, /// Amount of tokens to delegate pub amount: u64, - /// Maximum lamports for compressible top-up. Transaction fails if exceeded. - /// When set, includes max_top_up in instruction data and marks owner writable. - pub max_top_up: Option, + /// Optional fee payer for rent top-ups. If not provided, owner pays. + pub fee_payer: Option, } /// # Approve Light Token via CPI: @@ -49,7 +48,7 @@ pub struct Approve { /// owner, /// system_program, /// amount: 100, -/// max_top_up: None, +/// fee_payer: None, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -60,7 +59,8 @@ pub struct ApproveCpi<'info> { pub owner: AccountInfo<'info>, pub system_program: AccountInfo<'info>, pub amount: u64, - pub max_top_up: Option, + /// Optional fee payer for rent top-ups. If not provided, owner pays. + pub fee_payer: Option>, } impl<'info> ApproveCpi<'info> { @@ -70,24 +70,46 @@ impl<'info> ApproveCpi<'info> { pub fn invoke(self) -> Result<(), ProgramError> { let instruction = Approve::from(&self).instruction()?; - let account_infos = [ - self.token_account, - self.delegate, - self.owner, - self.system_program, - ]; - invoke(&instruction, &account_infos) + if let Some(fee_payer) = self.fee_payer { + let account_infos = [ + self.token_account, + self.delegate, + self.owner, + self.system_program, + fee_payer, + ]; + invoke(&instruction, &account_infos) + } else { + let account_infos = [ + self.token_account, + self.delegate, + self.owner, + self.system_program, + ]; + invoke(&instruction, &account_infos) + } } pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { let instruction = Approve::from(&self).instruction()?; - let account_infos = [ - self.token_account, - self.delegate, - self.owner, - self.system_program, - ]; - invoke_signed(&instruction, &account_infos, signer_seeds) + if let Some(fee_payer) = self.fee_payer { + let account_infos = [ + self.token_account, + self.delegate, + self.owner, + self.system_program, + fee_payer, + ]; + invoke_signed(&instruction, &account_infos, signer_seeds) + } else { + let account_infos = [ + self.token_account, + self.delegate, + self.owner, + self.system_program, + ]; + invoke_signed(&instruction, &account_infos, signer_seeds) + } } } @@ -98,7 +120,7 @@ impl<'info> From<&ApproveCpi<'info>> for Approve { delegate: *cpi.delegate.key, owner: *cpi.owner.key, amount: cpi.amount, - max_top_up: cpi.max_top_up, + fee_payer: cpi.fee_payer.as_ref().map(|a| *a.key), } } } @@ -107,24 +129,29 @@ impl Approve { pub fn instruction(self) -> Result { let mut data = vec![4u8]; // CTokenApprove discriminator data.extend_from_slice(&self.amount.to_le_bytes()); - if let Some(max_top_up) = self.max_top_up { - data.extend_from_slice(&max_top_up.to_le_bytes()); - } + data.extend_from_slice(&u16::MAX.to_le_bytes()); - let owner_meta = if self.max_top_up.is_some() { + // Owner is writable only when no fee_payer (owner pays for top-ups) + let owner_meta = if self.fee_payer.is_none() { AccountMeta::new(self.owner, true) } else { AccountMeta::new_readonly(self.owner, true) }; + let mut accounts = vec![ + AccountMeta::new(self.token_account, false), + AccountMeta::new_readonly(self.delegate, false), + owner_meta, + AccountMeta::new_readonly(Pubkey::default(), false), + ]; + + if let Some(fee_payer) = self.fee_payer { + accounts.push(AccountMeta::new(fee_payer, true)); + } + Ok(Instruction { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), - accounts: vec![ - AccountMeta::new(self.token_account, false), - AccountMeta::new_readonly(self.delegate, false), - owner_meta, - AccountMeta::new_readonly(Pubkey::default(), false), - ], + accounts, data, }) } diff --git a/sdk-libs/token-sdk/src/instruction/burn.rs b/sdk-libs/token-sdk/src/instruction/burn.rs index f7300e2867..0e9d7a3980 100644 --- a/sdk-libs/token-sdk/src/instruction/burn.rs +++ b/sdk-libs/token-sdk/src/instruction/burn.rs @@ -17,7 +17,6 @@ use solana_pubkey::Pubkey; /// mint, /// amount: 100, /// authority, -/// max_top_up: None, /// fee_payer: None, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -31,9 +30,6 @@ pub struct Burn { pub amount: u64, /// Owner of the Light Token account pub authority: Pubkey, - /// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (u16::MAX = no limit, 0 = no top-ups allowed) - /// When set (Some), includes max_top_up in instruction data - pub max_top_up: Option, /// Optional fee payer for rent top-ups. If not provided, authority pays. pub fee_payer: Option, } @@ -52,7 +48,6 @@ pub struct Burn { /// amount: 100, /// authority, /// system_program, -/// max_top_up: None, /// fee_payer: None, /// } /// .invoke()?; @@ -64,8 +59,6 @@ pub struct BurnCpi<'info> { pub amount: u64, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (u16::MAX = no limit, 0 = no top-ups allowed) - pub max_top_up: Option, /// Optional fee payer for rent top-ups. If not provided, authority pays. pub fee_payer: Option>, } @@ -117,7 +110,6 @@ impl<'info> From<&BurnCpi<'info>> for Burn { mint: *cpi.mint.key, amount: cpi.amount, authority: *cpi.authority.key, - max_top_up: cpi.max_top_up, fee_payer: cpi.fee_payer.as_ref().map(|a| *a.key), } } @@ -125,9 +117,8 @@ impl<'info> From<&BurnCpi<'info>> for Burn { impl Burn { pub fn instruction(self) -> Result { - // Authority is writable only when max_top_up is set AND no fee_payer - // (authority pays for top-ups only if no separate fee_payer) - let authority_meta = if self.max_top_up.is_some() && self.fee_payer.is_none() { + // Authority is writable only when no fee_payer (authority pays for top-ups) + let authority_meta = if self.fee_payer.is_none() { AccountMeta::new(self.authority, true) } else { AccountMeta::new_readonly(self.authority, true) @@ -152,10 +143,7 @@ impl Burn { data: { let mut data = vec![8u8]; // CTokenBurn discriminator data.extend_from_slice(&self.amount.to_le_bytes()); - // Include max_top_up if set (10-byte format) - if let Some(max_top_up) = self.max_top_up { - data.extend_from_slice(&max_top_up.to_le_bytes()); - } + data.extend_from_slice(&u16::MAX.to_le_bytes()); data }, }) diff --git a/sdk-libs/token-sdk/src/instruction/burn_checked.rs b/sdk-libs/token-sdk/src/instruction/burn_checked.rs index d4f5163764..950850714f 100644 --- a/sdk-libs/token-sdk/src/instruction/burn_checked.rs +++ b/sdk-libs/token-sdk/src/instruction/burn_checked.rs @@ -18,7 +18,6 @@ use solana_pubkey::Pubkey; /// amount: 100, /// decimals: 8, /// authority, -/// max_top_up: None, /// fee_payer: None, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -34,9 +33,6 @@ pub struct BurnChecked { pub decimals: u8, /// Owner of the Light Token account pub authority: Pubkey, - /// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (u16::MAX = no limit, 0 = no top-ups allowed) - /// When set (Some), includes max_top_up in instruction data - pub max_top_up: Option, /// Optional fee payer for rent top-ups. If not provided, authority pays. pub fee_payer: Option, } @@ -56,7 +52,6 @@ pub struct BurnChecked { /// decimals: 8, /// authority, /// system_program, -/// max_top_up: None, /// fee_payer: None, /// } /// .invoke()?; @@ -69,8 +64,6 @@ pub struct BurnCheckedCpi<'info> { pub decimals: u8, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (u16::MAX = no limit, 0 = no top-ups allowed) - pub max_top_up: Option, /// Optional fee payer for rent top-ups. If not provided, authority pays. pub fee_payer: Option>, } @@ -123,7 +116,6 @@ impl<'info> From<&BurnCheckedCpi<'info>> for BurnChecked { amount: cpi.amount, decimals: cpi.decimals, authority: *cpi.authority.key, - max_top_up: cpi.max_top_up, fee_payer: cpi.fee_payer.as_ref().map(|a| *a.key), } } @@ -131,9 +123,8 @@ impl<'info> From<&BurnCheckedCpi<'info>> for BurnChecked { impl BurnChecked { pub fn instruction(self) -> Result { - // Authority is writable only when max_top_up is set AND no fee_payer - // (authority pays for top-ups only if no separate fee_payer) - let authority_meta = if self.max_top_up.is_some() && self.fee_payer.is_none() { + // Authority is writable only when no fee_payer (authority pays for top-ups) + let authority_meta = if self.fee_payer.is_none() { AccountMeta::new(self.authority, true) } else { AccountMeta::new_readonly(self.authority, true) @@ -159,10 +150,7 @@ impl BurnChecked { let mut data = vec![15u8]; // CTokenBurnChecked discriminator data.extend_from_slice(&self.amount.to_le_bytes()); data.push(self.decimals); - // Include max_top_up if set (11-byte format) - if let Some(max_top_up) = self.max_top_up { - data.extend_from_slice(&max_top_up.to_le_bytes()); - } + data.extend_from_slice(&u16::MAX.to_le_bytes()); data }, }) diff --git a/sdk-libs/token-sdk/src/instruction/mint_to.rs b/sdk-libs/token-sdk/src/instruction/mint_to.rs index ef6480017f..2976b490ef 100644 --- a/sdk-libs/token-sdk/src/instruction/mint_to.rs +++ b/sdk-libs/token-sdk/src/instruction/mint_to.rs @@ -17,7 +17,6 @@ use solana_pubkey::Pubkey; /// destination, /// amount: 100, /// authority, -/// max_top_up: None, /// fee_payer: None, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -31,9 +30,6 @@ pub struct MintTo { pub amount: u64, /// Mint authority pub authority: Pubkey, - /// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (u16::MAX = no limit, 0 = no top-ups allowed) - /// When set (Some), includes max_top_up in instruction data - pub max_top_up: Option, /// Optional fee payer for rent top-ups. If not provided, authority pays. pub fee_payer: Option, } @@ -52,7 +48,6 @@ pub struct MintTo { /// amount: 100, /// authority, /// system_program, -/// max_top_up: None, /// fee_payer: None, /// } /// .invoke()?; @@ -64,8 +59,6 @@ pub struct MintToCpi<'info> { pub amount: u64, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (u16::MAX = no limit, 0 = no top-ups allowed) - pub max_top_up: Option, /// Optional fee payer for rent top-ups. If not provided, authority pays. pub fee_payer: Option>, } @@ -127,7 +120,6 @@ impl<'info> From<&MintToCpi<'info>> for MintTo { destination: *cpi.destination.key, amount: cpi.amount, authority: *cpi.authority.key, - max_top_up: cpi.max_top_up, fee_payer: cpi.fee_payer.as_ref().map(|a| *a.key), } } @@ -135,9 +127,8 @@ impl<'info> From<&MintToCpi<'info>> for MintTo { impl MintTo { pub fn instruction(self) -> Result { - // Authority is writable only when max_top_up is set AND no fee_payer - // (authority pays for top-ups only if no separate fee_payer) - let authority_meta = if self.max_top_up.is_some() && self.fee_payer.is_none() { + // Authority is writable only when no fee_payer (authority pays for top-ups) + let authority_meta = if self.fee_payer.is_none() { AccountMeta::new(self.authority, true) } else { AccountMeta::new_readonly(self.authority, true) @@ -162,10 +153,7 @@ impl MintTo { data: { let mut data = vec![7u8]; // MintTo discriminator data.extend_from_slice(&self.amount.to_le_bytes()); - // Include max_top_up if set (10-byte format) - if let Some(max_top_up) = self.max_top_up { - data.extend_from_slice(&max_top_up.to_le_bytes()); - } + data.extend_from_slice(&u16::MAX.to_le_bytes()); data }, }) diff --git a/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs b/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs index 775d506314..bad8487112 100644 --- a/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs +++ b/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs @@ -18,7 +18,6 @@ use solana_pubkey::Pubkey; /// amount: 100, /// decimals: 8, /// authority, -/// max_top_up: None, /// fee_payer: None, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -34,9 +33,6 @@ pub struct MintToChecked { pub decimals: u8, /// Mint authority pub authority: Pubkey, - /// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (u16::MAX = no limit, 0 = no top-ups allowed) - /// When set (Some), includes max_top_up in instruction data - pub max_top_up: Option, /// Optional fee payer for rent top-ups. If not provided, authority pays. pub fee_payer: Option, } @@ -56,7 +52,6 @@ pub struct MintToChecked { /// decimals: 8, /// authority, /// system_program, -/// max_top_up: None, /// fee_payer: None, /// } /// .invoke()?; @@ -69,8 +64,6 @@ pub struct MintToCheckedCpi<'info> { pub decimals: u8, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (u16::MAX = no limit, 0 = no top-ups allowed) - pub max_top_up: Option, /// Optional fee payer for rent top-ups. If not provided, authority pays. pub fee_payer: Option>, } @@ -133,7 +126,6 @@ impl<'info> From<&MintToCheckedCpi<'info>> for MintToChecked { amount: cpi.amount, decimals: cpi.decimals, authority: *cpi.authority.key, - max_top_up: cpi.max_top_up, fee_payer: cpi.fee_payer.as_ref().map(|a| *a.key), } } @@ -141,9 +133,8 @@ impl<'info> From<&MintToCheckedCpi<'info>> for MintToChecked { impl MintToChecked { pub fn instruction(self) -> Result { - // Authority is writable only when max_top_up is set AND no fee_payer - // (authority pays for top-ups only if no separate fee_payer) - let authority_meta = if self.max_top_up.is_some() && self.fee_payer.is_none() { + // Authority is writable only when no fee_payer (authority pays for top-ups) + let authority_meta = if self.fee_payer.is_none() { AccountMeta::new(self.authority, true) } else { AccountMeta::new_readonly(self.authority, true) @@ -169,10 +160,7 @@ impl MintToChecked { let mut data = vec![14u8]; // TokenMintToChecked discriminator data.extend_from_slice(&self.amount.to_le_bytes()); data.push(self.decimals); - // Include max_top_up if set (11-byte format) - if let Some(max_top_up) = self.max_top_up { - data.extend_from_slice(&max_top_up.to_le_bytes()); - } + data.extend_from_slice(&u16::MAX.to_le_bytes()); data }, }) diff --git a/sdk-libs/token-sdk/src/instruction/revoke.rs b/sdk-libs/token-sdk/src/instruction/revoke.rs index 6fa8a4b280..bba21f3365 100644 --- a/sdk-libs/token-sdk/src/instruction/revoke.rs +++ b/sdk-libs/token-sdk/src/instruction/revoke.rs @@ -14,18 +14,17 @@ use solana_pubkey::Pubkey; /// let instruction = Revoke { /// token_account, /// owner, -/// max_top_up: None, +/// fee_payer: None, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` pub struct Revoke { /// Light Token account to revoke delegation for pub token_account: Pubkey, - /// Owner of the Light Token account (signer, payer for top-up) + /// Owner of the Light Token account (signer) pub owner: Pubkey, - /// Maximum lamports for compressible top-up. Transaction fails if exceeded. - /// When set, includes max_top_up in instruction data and marks owner writable. - pub max_top_up: Option, + /// Optional fee payer for rent top-ups. If not provided, owner pays. + pub fee_payer: Option, } /// # Revoke Light Token via CPI: @@ -39,7 +38,7 @@ pub struct Revoke { /// token_account, /// owner, /// system_program, -/// max_top_up: None, +/// fee_payer: None, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -48,7 +47,8 @@ pub struct RevokeCpi<'info> { pub token_account: AccountInfo<'info>, pub owner: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - pub max_top_up: Option, + /// Optional fee payer for rent top-ups. If not provided, owner pays. + pub fee_payer: Option>, } impl<'info> RevokeCpi<'info> { @@ -58,14 +58,24 @@ impl<'info> RevokeCpi<'info> { pub fn invoke(self) -> Result<(), ProgramError> { let instruction = Revoke::from(&self).instruction()?; - let account_infos = [self.token_account, self.owner, self.system_program]; - invoke(&instruction, &account_infos) + if let Some(fee_payer) = self.fee_payer { + let account_infos = [self.token_account, self.owner, self.system_program, fee_payer]; + invoke(&instruction, &account_infos) + } else { + let account_infos = [self.token_account, self.owner, self.system_program]; + invoke(&instruction, &account_infos) + } } pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { let instruction = Revoke::from(&self).instruction()?; - let account_infos = [self.token_account, self.owner, self.system_program]; - invoke_signed(&instruction, &account_infos, signer_seeds) + if let Some(fee_payer) = self.fee_payer { + let account_infos = [self.token_account, self.owner, self.system_program, fee_payer]; + invoke_signed(&instruction, &account_infos, signer_seeds) + } else { + let account_infos = [self.token_account, self.owner, self.system_program]; + invoke_signed(&instruction, &account_infos, signer_seeds) + } } } @@ -74,7 +84,7 @@ impl<'info> From<&RevokeCpi<'info>> for Revoke { Self { token_account: *cpi.token_account.key, owner: *cpi.owner.key, - max_top_up: cpi.max_top_up, + fee_payer: cpi.fee_payer.as_ref().map(|a| *a.key), } } } @@ -82,23 +92,28 @@ impl<'info> From<&RevokeCpi<'info>> for Revoke { impl Revoke { pub fn instruction(self) -> Result { let mut data = vec![5u8]; // CTokenRevoke discriminator - if let Some(max_top_up) = self.max_top_up { - data.extend_from_slice(&max_top_up.to_le_bytes()); - } + data.extend_from_slice(&u16::MAX.to_le_bytes()); - let owner_meta = if self.max_top_up.is_some() { + // Owner is writable only when no fee_payer (owner pays for top-ups) + let owner_meta = if self.fee_payer.is_none() { AccountMeta::new(self.owner, true) } else { AccountMeta::new_readonly(self.owner, true) }; + let mut accounts = vec![ + AccountMeta::new(self.token_account, false), + owner_meta, + AccountMeta::new_readonly(Pubkey::default(), false), + ]; + + if let Some(fee_payer) = self.fee_payer { + accounts.push(AccountMeta::new(fee_payer, true)); + } + Ok(Instruction { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), - accounts: vec![ - AccountMeta::new(self.token_account, false), - owner_meta, - AccountMeta::new_readonly(Pubkey::default(), false), - ], + accounts, data, }) } diff --git a/sdk-libs/token-sdk/src/instruction/transfer.rs b/sdk-libs/token-sdk/src/instruction/transfer.rs index 90985229a2..beeb3e4c0d 100644 --- a/sdk-libs/token-sdk/src/instruction/transfer.rs +++ b/sdk-libs/token-sdk/src/instruction/transfer.rs @@ -17,7 +17,6 @@ use solana_pubkey::Pubkey; /// destination, /// amount: 100, /// authority, -/// max_top_up: None, /// fee_payer: None, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -27,9 +26,6 @@ pub struct Transfer { pub destination: Pubkey, pub amount: u64, pub authority: Pubkey, - /// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (u16::MAX = no limit, 0 = no top-ups allowed) - /// When set, includes max_top_up in instruction data and adds system program account for compressible top-up - pub max_top_up: Option, /// Optional fee payer for rent top-ups. If not provided, authority pays. /// When set, fee_payer pays for top-ups instead of authority. pub fee_payer: Option, @@ -49,7 +45,6 @@ pub struct Transfer { /// amount: 100, /// authority, /// system_program, -/// max_top_up: None, /// fee_payer: None, /// } /// .invoke()?; @@ -61,8 +56,6 @@ pub struct TransferCpi<'info> { pub amount: u64, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (u16::MAX = no limit, 0 = no top-ups allowed) - pub max_top_up: Option, /// Optional fee payer for rent top-ups. If not provided, authority pays. pub fee_payer: Option>, } @@ -124,7 +117,6 @@ impl<'info> From<&TransferCpi<'info>> for Transfer { destination: *account_infos.destination.key, amount: account_infos.amount, authority: *account_infos.authority.key, - max_top_up: account_infos.max_top_up, fee_payer: account_infos.fee_payer.as_ref().map(|a| *a.key), } } @@ -132,9 +124,8 @@ impl<'info> From<&TransferCpi<'info>> for Transfer { impl Transfer { pub fn instruction(self) -> Result { - // Authority is writable only when max_top_up is set AND no fee_payer - // (authority pays for top-ups only if no separate fee_payer) - let authority_meta = if self.max_top_up.is_some() && self.fee_payer.is_none() { + // Authority is writable only when no fee_payer (authority pays for top-ups) + let authority_meta = if self.fee_payer.is_none() { AccountMeta::new(self.authority, true) } else { AccountMeta::new_readonly(self.authority, true) @@ -159,10 +150,7 @@ impl Transfer { data: { let mut data = vec![3u8]; data.extend_from_slice(&self.amount.to_le_bytes()); - // Include max_top_up if set (10-byte format) - if let Some(max_top_up) = self.max_top_up { - data.extend_from_slice(&max_top_up.to_le_bytes()); - } + data.extend_from_slice(&u16::MAX.to_le_bytes()); data }, }) diff --git a/sdk-libs/token-sdk/src/instruction/transfer_checked.rs b/sdk-libs/token-sdk/src/instruction/transfer_checked.rs index 0a8b088f38..acdf0ad1a9 100644 --- a/sdk-libs/token-sdk/src/instruction/transfer_checked.rs +++ b/sdk-libs/token-sdk/src/instruction/transfer_checked.rs @@ -20,7 +20,6 @@ use solana_pubkey::Pubkey; /// amount: 100, /// decimals: 9, /// authority, -/// max_top_up: None, /// fee_payer: None, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -32,9 +31,6 @@ pub struct TransferChecked { pub amount: u64, pub decimals: u8, pub authority: Pubkey, - /// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (u16::MAX = no limit, 0 = no top-ups allowed) - /// When set (Some), includes max_top_up in instruction data - pub max_top_up: Option, /// Optional fee payer for rent top-ups. If not provided, authority pays. pub fee_payer: Option, } @@ -56,7 +52,6 @@ pub struct TransferChecked { /// decimals: 9, /// authority, /// system_program, -/// max_top_up: None, /// fee_payer: None, /// } /// .invoke()?; @@ -70,8 +65,6 @@ pub struct TransferCheckedCpi<'info> { pub decimals: u8, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (u16::MAX = no limit, 0 = no top-ups allowed) - pub max_top_up: Option, /// Optional fee payer for rent top-ups. If not provided, authority pays. pub fee_payer: Option>, } @@ -139,7 +132,6 @@ impl<'info> From<&TransferCheckedCpi<'info>> for TransferChecked { amount: account_infos.amount, decimals: account_infos.decimals, authority: *account_infos.authority.key, - max_top_up: account_infos.max_top_up, fee_payer: account_infos.fee_payer.as_ref().map(|a| *a.key), } } @@ -147,9 +139,8 @@ impl<'info> From<&TransferCheckedCpi<'info>> for TransferChecked { impl TransferChecked { pub fn instruction(self) -> Result { - // Authority is writable only when max_top_up is set AND no fee_payer - // (authority pays for top-ups only if no separate fee_payer) - let authority_meta = if self.max_top_up.is_some() && self.fee_payer.is_none() { + // Authority is writable only when no fee_payer (authority pays for top-ups) + let authority_meta = if self.fee_payer.is_none() { AccountMeta::new(self.authority, true) } else { AccountMeta::new_readonly(self.authority, true) @@ -173,14 +164,10 @@ impl TransferChecked { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), accounts, data: { - // Discriminator (1) + amount (8) + decimals (1) + optional max_top_up (2) let mut data = vec![12u8]; // TransferChecked discriminator (SPL compatible) data.extend_from_slice(&self.amount.to_le_bytes()); data.push(self.decimals); - // Include max_top_up if set (11-byte format) - if let Some(max_top_up) = self.max_top_up { - data.extend_from_slice(&max_top_up.to_le_bytes()); - } + data.extend_from_slice(&u16::MAX.to_le_bytes()); data }, }) diff --git a/sdk-libs/token-sdk/src/instruction/transfer_interface.rs b/sdk-libs/token-sdk/src/instruction/transfer_interface.rs index 2fa38c0e63..b4cb36b85a 100644 --- a/sdk-libs/token-sdk/src/instruction/transfer_interface.rs +++ b/sdk-libs/token-sdk/src/instruction/transfer_interface.rs @@ -102,7 +102,6 @@ pub struct SplInterfaceCpi<'info> { /// authority, /// payer, /// spl_interface: None, -/// max_top_up: None, /// source_owner: LIGHT_TOKEN_PROGRAM_ID, /// destination_owner: LIGHT_TOKEN_PROGRAM_ID, /// }.instruction()?; @@ -116,8 +115,6 @@ pub struct TransferInterface { pub authority: Pubkey, pub payer: Pubkey, pub spl_interface: Option, - /// Maximum lamports for rent and top-up combined (for light->light transfers) - pub max_top_up: Option, /// Owner of the source account (used to determine transfer type) pub source_owner: Pubkey, /// Owner of the destination account (used to determine transfer type) @@ -133,7 +130,6 @@ impl TransferInterface { destination: self.destination, amount: self.amount, authority: self.authority, - max_top_up: self.max_top_up, fee_payer: Some(self.payer), } .instruction(), @@ -212,7 +208,6 @@ impl<'info> From<&TransferInterfaceCpi<'info>> for TransferInterface { authority: *cpi.authority.key, payer: *cpi.payer.key, spl_interface: cpi.spl_interface.as_ref().map(SplInterface::from), - max_top_up: None, source_owner: *cpi.source_account.owner, destination_owner: *cpi.destination_account.owner, } diff --git a/sdk-tests/sdk-anchor-test/target/idl/sdk_anchor_test.json b/sdk-tests/sdk-anchor-test/target/idl/sdk_anchor_test.json deleted file mode 100644 index 0e4e427c40..0000000000 --- a/sdk-tests/sdk-anchor-test/target/idl/sdk_anchor_test.json +++ /dev/null @@ -1,726 +0,0 @@ -{ - "address": "2tzfijPBGbrR5PboyFUFKzfEoLTwdDSHUjANCw929wyt", - "metadata": { - "name": "sdk_anchor_test", - "version": "0.7.0", - "spec": "0.1.0", - "description": "Test program for Light SDK and Light Macros" - }, - "instructions": [ - { - "name": "close_compressed_account", - "discriminator": [ - 55, - 108, - 99, - 108, - 119, - 228, - 247, - 203 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - } - ], - "args": [ - { - "name": "proof", - "type": { - "defined": { - "name": "ValidityProof" - } - } - }, - { - "name": "my_compressed_account", - "type": { - "defined": { - "name": "MyCompressedAccount" - } - } - }, - { - "name": "account_meta", - "type": { - "defined": { - "name": "CompressedAccountMeta" - } - } - } - ] - }, - { - "name": "close_compressed_account_permanent", - "discriminator": [ - 117, - 145, - 242, - 98, - 46, - 187, - 118, - 125 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - } - ], - "args": [ - { - "name": "proof", - "type": { - "defined": { - "name": "ValidityProof" - } - } - }, - { - "name": "account_meta", - "type": { - "defined": { - "name": "CompressedAccountMetaBurn" - } - } - } - ] - }, - { - "name": "close_compressed_account_v2", - "discriminator": [ - 12, - 21, - 104, - 30, - 185, - 99, - 10, - 30 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - } - ], - "args": [ - { - "name": "proof", - "type": { - "defined": { - "name": "ValidityProof" - } - } - }, - { - "name": "my_compressed_account", - "type": { - "defined": { - "name": "MyCompressedAccount" - } - } - }, - { - "name": "account_meta", - "type": { - "defined": { - "name": "CompressedAccountMeta" - } - } - } - ] - }, - { - "name": "create_compressed_account", - "discriminator": [ - 74, - 87, - 131, - 150, - 204, - 209, - 66, - 94 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - } - ], - "args": [ - { - "name": "proof", - "type": { - "defined": { - "name": "ValidityProof" - } - } - }, - { - "name": "address_tree_info", - "type": { - "defined": { - "name": "PackedAddressTreeInfo" - } - } - }, - { - "name": "output_tree_index", - "type": "u8" - }, - { - "name": "name", - "type": "string" - } - ] - }, - { - "name": "create_compressed_account_v2", - "discriminator": [ - 16, - 69, - 137, - 87, - 207, - 37, - 81, - 138 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - } - ], - "args": [ - { - "name": "proof", - "type": { - "defined": { - "name": "ValidityProof" - } - } - }, - { - "name": "address_tree_info", - "type": { - "defined": { - "name": "PackedAddressTreeInfo" - } - } - }, - { - "name": "output_tree_index", - "type": "u8" - }, - { - "name": "name", - "type": "string" - } - ] - }, - { - "name": "reinit_closed_account", - "discriminator": [ - 100, - 26, - 249, - 27, - 243, - 0, - 206, - 64 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - } - ], - "args": [ - { - "name": "proof", - "type": { - "defined": { - "name": "ValidityProof" - } - } - }, - { - "name": "account_meta", - "type": { - "defined": { - "name": "CompressedAccountMeta" - } - } - } - ] - }, - { - "name": "update_compressed_account", - "discriminator": [ - 3, - 98, - 6, - 60, - 116, - 45, - 88, - 166 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - } - ], - "args": [ - { - "name": "proof", - "type": { - "defined": { - "name": "ValidityProof" - } - } - }, - { - "name": "my_compressed_account", - "type": { - "defined": { - "name": "MyCompressedAccount" - } - } - }, - { - "name": "account_meta", - "type": { - "defined": { - "name": "CompressedAccountMeta" - } - } - }, - { - "name": "nested_data", - "type": { - "defined": { - "name": "NestedData" - } - } - } - ] - }, - { - "name": "update_compressed_account_v2", - "discriminator": [ - 100, - 134, - 47, - 184, - 220, - 7, - 96, - 236 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - } - ], - "args": [ - { - "name": "proof", - "type": { - "defined": { - "name": "ValidityProof" - } - } - }, - { - "name": "my_compressed_account", - "type": { - "defined": { - "name": "MyCompressedAccount" - } - } - }, - { - "name": "account_meta", - "type": { - "defined": { - "name": "CompressedAccountMeta" - } - } - }, - { - "name": "nested_data", - "type": { - "defined": { - "name": "NestedData" - } - } - } - ] - }, - { - "name": "without_compressed_account", - "discriminator": [ - 68, - 84, - 81, - 196, - 24, - 131, - 208, - 209 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - }, - { - "name": "my_regular_account", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 99, - 111, - 109, - 112, - 114, - 101, - 115, - 115, - 101, - 100 - ] - }, - { - "kind": "arg", - "path": "name" - } - ] - } - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - } - ], - "args": [ - { - "name": "name", - "type": "string" - } - ] - } - ], - "accounts": [ - { - "name": "MyRegularAccount", - "discriminator": [ - 186, - 181, - 76, - 117, - 61, - 130, - 63, - 14 - ] - } - ], - "events": [ - { - "name": "MyCompressedAccount", - "discriminator": [ - 147, - 40, - 99, - 80, - 53, - 44, - 10, - 210 - ] - } - ], - "types": [ - { - "name": "CompressedAccountMeta", - "type": { - "kind": "struct", - "fields": [ - { - "name": "tree_info", - "docs": [ - "Merkle tree context." - ], - "type": { - "defined": { - "name": "PackedStateTreeInfo" - } - } - }, - { - "name": "address", - "docs": [ - "Address." - ], - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "output_state_tree_index", - "docs": [ - "Output merkle tree index." - ], - "type": "u8" - } - ] - } - }, - { - "name": "CompressedAccountMetaBurn", - "type": { - "kind": "struct", - "fields": [ - { - "name": "tree_info", - "docs": [ - "State Merkle tree context." - ], - "type": { - "defined": { - "name": "PackedStateTreeInfo" - } - } - }, - { - "name": "address", - "docs": [ - "Address." - ], - "type": { - "array": [ - "u8", - 32 - ] - } - } - ] - } - }, - { - "name": "CompressedProof", - "repr": { - "kind": "c" - }, - "type": { - "kind": "struct", - "fields": [ - { - "name": "a", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "b", - "type": { - "array": [ - "u8", - 64 - ] - } - }, - { - "name": "c", - "type": { - "array": [ - "u8", - 32 - ] - } - } - ] - } - }, - { - "name": "MyCompressedAccount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "name", - "type": "string" - }, - { - "name": "nested", - "type": { - "defined": { - "name": "NestedData" - } - } - } - ] - } - }, - { - "name": "MyRegularAccount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "name", - "type": "string" - } - ] - } - }, - { - "name": "NestedData", - "type": { - "kind": "struct", - "fields": [ - { - "name": "one", - "type": "u16" - }, - { - "name": "two", - "type": "u16" - }, - { - "name": "three", - "type": "u16" - }, - { - "name": "four", - "type": "u16" - }, - { - "name": "five", - "type": "u16" - }, - { - "name": "six", - "type": "u16" - }, - { - "name": "seven", - "type": "u16" - }, - { - "name": "eight", - "type": "u16" - }, - { - "name": "nine", - "type": "u16" - }, - { - "name": "ten", - "type": "u16" - }, - { - "name": "eleven", - "type": "u16" - }, - { - "name": "twelve", - "type": "u16" - } - ] - } - }, - { - "name": "PackedAddressTreeInfo", - "type": { - "kind": "struct", - "fields": [ - { - "name": "address_merkle_tree_pubkey_index", - "type": "u8" - }, - { - "name": "address_queue_pubkey_index", - "type": "u8" - }, - { - "name": "root_index", - "type": "u16" - } - ] - } - }, - { - "name": "PackedStateTreeInfo", - "type": { - "kind": "struct", - "fields": [ - { - "name": "root_index", - "type": "u16" - }, - { - "name": "prove_by_index", - "type": "bool" - }, - { - "name": "merkle_tree_pubkey_index", - "type": "u8" - }, - { - "name": "queue_pubkey_index", - "type": "u8" - }, - { - "name": "leaf_index", - "type": "u32" - } - ] - } - }, - { - "name": "ValidityProof", - "type": { - "kind": "struct", - "fields": [ - { - "option": { - "defined": { - "name": "CompressedProof" - } - } - } - ] - } - } - ] -} \ No newline at end of file diff --git a/sdk-tests/sdk-anchor-test/target/types/sdk_anchor_test.ts b/sdk-tests/sdk-anchor-test/target/types/sdk_anchor_test.ts deleted file mode 100644 index e805730cc1..0000000000 --- a/sdk-tests/sdk-anchor-test/target/types/sdk_anchor_test.ts +++ /dev/null @@ -1,732 +0,0 @@ -/** - * Program IDL in camelCase format in order to be used in JS/TS. - * - * Note that this is only a type helper and is not the actual IDL. The original - * IDL can be found at `target/idl/sdk_anchor_test.json`. - */ -export type SdkAnchorTest = { - "address": "2tzfijPBGbrR5PboyFUFKzfEoLTwdDSHUjANCw929wyt", - "metadata": { - "name": "sdkAnchorTest", - "version": "0.7.0", - "spec": "0.1.0", - "description": "Test program for Light SDK and Light Macros" - }, - "instructions": [ - { - "name": "closeCompressedAccount", - "discriminator": [ - 55, - 108, - 99, - 108, - 119, - 228, - 247, - 203 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - } - ], - "args": [ - { - "name": "proof", - "type": { - "defined": { - "name": "validityProof" - } - } - }, - { - "name": "myCompressedAccount", - "type": { - "defined": { - "name": "myCompressedAccount" - } - } - }, - { - "name": "accountMeta", - "type": { - "defined": { - "name": "compressedAccountMeta" - } - } - } - ] - }, - { - "name": "closeCompressedAccountPermanent", - "discriminator": [ - 117, - 145, - 242, - 98, - 46, - 187, - 118, - 125 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - } - ], - "args": [ - { - "name": "proof", - "type": { - "defined": { - "name": "validityProof" - } - } - }, - { - "name": "accountMeta", - "type": { - "defined": { - "name": "compressedAccountMetaBurn" - } - } - } - ] - }, - { - "name": "closeCompressedAccountV2", - "discriminator": [ - 12, - 21, - 104, - 30, - 185, - 99, - 10, - 30 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - } - ], - "args": [ - { - "name": "proof", - "type": { - "defined": { - "name": "validityProof" - } - } - }, - { - "name": "myCompressedAccount", - "type": { - "defined": { - "name": "myCompressedAccount" - } - } - }, - { - "name": "accountMeta", - "type": { - "defined": { - "name": "compressedAccountMeta" - } - } - } - ] - }, - { - "name": "createCompressedAccount", - "discriminator": [ - 74, - 87, - 131, - 150, - 204, - 209, - 66, - 94 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - } - ], - "args": [ - { - "name": "proof", - "type": { - "defined": { - "name": "validityProof" - } - } - }, - { - "name": "addressTreeInfo", - "type": { - "defined": { - "name": "packedAddressTreeInfo" - } - } - }, - { - "name": "outputTreeIndex", - "type": "u8" - }, - { - "name": "name", - "type": "string" - } - ] - }, - { - "name": "createCompressedAccountV2", - "discriminator": [ - 16, - 69, - 137, - 87, - 207, - 37, - 81, - 138 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - } - ], - "args": [ - { - "name": "proof", - "type": { - "defined": { - "name": "validityProof" - } - } - }, - { - "name": "addressTreeInfo", - "type": { - "defined": { - "name": "packedAddressTreeInfo" - } - } - }, - { - "name": "outputTreeIndex", - "type": "u8" - }, - { - "name": "name", - "type": "string" - } - ] - }, - { - "name": "reinitClosedAccount", - "discriminator": [ - 100, - 26, - 249, - 27, - 243, - 0, - 206, - 64 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - } - ], - "args": [ - { - "name": "proof", - "type": { - "defined": { - "name": "validityProof" - } - } - }, - { - "name": "accountMeta", - "type": { - "defined": { - "name": "compressedAccountMeta" - } - } - } - ] - }, - { - "name": "updateCompressedAccount", - "discriminator": [ - 3, - 98, - 6, - 60, - 116, - 45, - 88, - 166 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - } - ], - "args": [ - { - "name": "proof", - "type": { - "defined": { - "name": "validityProof" - } - } - }, - { - "name": "myCompressedAccount", - "type": { - "defined": { - "name": "myCompressedAccount" - } - } - }, - { - "name": "accountMeta", - "type": { - "defined": { - "name": "compressedAccountMeta" - } - } - }, - { - "name": "nestedData", - "type": { - "defined": { - "name": "nestedData" - } - } - } - ] - }, - { - "name": "updateCompressedAccountV2", - "discriminator": [ - 100, - 134, - 47, - 184, - 220, - 7, - 96, - 236 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - } - ], - "args": [ - { - "name": "proof", - "type": { - "defined": { - "name": "validityProof" - } - } - }, - { - "name": "myCompressedAccount", - "type": { - "defined": { - "name": "myCompressedAccount" - } - } - }, - { - "name": "accountMeta", - "type": { - "defined": { - "name": "compressedAccountMeta" - } - } - }, - { - "name": "nestedData", - "type": { - "defined": { - "name": "nestedData" - } - } - } - ] - }, - { - "name": "withoutCompressedAccount", - "discriminator": [ - 68, - 84, - 81, - 196, - 24, - 131, - 208, - 209 - ], - "accounts": [ - { - "name": "signer", - "writable": true, - "signer": true - }, - { - "name": "myRegularAccount", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 99, - 111, - 109, - 112, - 114, - 101, - 115, - 115, - 101, - 100 - ] - }, - { - "kind": "arg", - "path": "name" - } - ] - } - }, - { - "name": "systemProgram", - "address": "11111111111111111111111111111111" - } - ], - "args": [ - { - "name": "name", - "type": "string" - } - ] - } - ], - "accounts": [ - { - "name": "myRegularAccount", - "discriminator": [ - 186, - 181, - 76, - 117, - 61, - 130, - 63, - 14 - ] - } - ], - "events": [ - { - "name": "myCompressedAccount", - "discriminator": [ - 147, - 40, - 99, - 80, - 53, - 44, - 10, - 210 - ] - } - ], - "types": [ - { - "name": "compressedAccountMeta", - "type": { - "kind": "struct", - "fields": [ - { - "name": "treeInfo", - "docs": [ - "Merkle tree context." - ], - "type": { - "defined": { - "name": "packedStateTreeInfo" - } - } - }, - { - "name": "address", - "docs": [ - "Address." - ], - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "outputStateTreeIndex", - "docs": [ - "Output merkle tree index." - ], - "type": "u8" - } - ] - } - }, - { - "name": "compressedAccountMetaBurn", - "type": { - "kind": "struct", - "fields": [ - { - "name": "treeInfo", - "docs": [ - "State Merkle tree context." - ], - "type": { - "defined": { - "name": "packedStateTreeInfo" - } - } - }, - { - "name": "address", - "docs": [ - "Address." - ], - "type": { - "array": [ - "u8", - 32 - ] - } - } - ] - } - }, - { - "name": "compressedProof", - "repr": { - "kind": "c" - }, - "type": { - "kind": "struct", - "fields": [ - { - "name": "a", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "b", - "type": { - "array": [ - "u8", - 64 - ] - } - }, - { - "name": "c", - "type": { - "array": [ - "u8", - 32 - ] - } - } - ] - } - }, - { - "name": "myCompressedAccount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "name", - "type": "string" - }, - { - "name": "nested", - "type": { - "defined": { - "name": "nestedData" - } - } - } - ] - } - }, - { - "name": "myRegularAccount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "name", - "type": "string" - } - ] - } - }, - { - "name": "nestedData", - "type": { - "kind": "struct", - "fields": [ - { - "name": "one", - "type": "u16" - }, - { - "name": "two", - "type": "u16" - }, - { - "name": "three", - "type": "u16" - }, - { - "name": "four", - "type": "u16" - }, - { - "name": "five", - "type": "u16" - }, - { - "name": "six", - "type": "u16" - }, - { - "name": "seven", - "type": "u16" - }, - { - "name": "eight", - "type": "u16" - }, - { - "name": "nine", - "type": "u16" - }, - { - "name": "ten", - "type": "u16" - }, - { - "name": "eleven", - "type": "u16" - }, - { - "name": "twelve", - "type": "u16" - } - ] - } - }, - { - "name": "packedAddressTreeInfo", - "type": { - "kind": "struct", - "fields": [ - { - "name": "addressMerkleTreePubkeyIndex", - "type": "u8" - }, - { - "name": "addressQueuePubkeyIndex", - "type": "u8" - }, - { - "name": "rootIndex", - "type": "u16" - } - ] - } - }, - { - "name": "packedStateTreeInfo", - "type": { - "kind": "struct", - "fields": [ - { - "name": "rootIndex", - "type": "u16" - }, - { - "name": "proveByIndex", - "type": "bool" - }, - { - "name": "merkleTreePubkeyIndex", - "type": "u8" - }, - { - "name": "queuePubkeyIndex", - "type": "u8" - }, - { - "name": "leafIndex", - "type": "u32" - } - ] - } - }, - { - "name": "validityProof", - "type": { - "kind": "struct", - "fields": [ - { - "option": { - "defined": { - "name": "compressedProof" - } - } - } - ] - } - } - ] -}; diff --git a/sdk-tests/sdk-light-token-pinocchio/src/approve.rs b/sdk-tests/sdk-light-token-pinocchio/src/approve.rs index 1950a65815..d97e3d47bd 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/approve.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/approve.rs @@ -36,7 +36,7 @@ pub fn process_approve_invoke( owner: &accounts[2], system_program: &accounts[3], amount: data.amount, - max_top_up: None, + fee_payer: None, } .invoke()?; @@ -75,7 +75,7 @@ pub fn process_approve_invoke_signed( owner: &accounts[2], system_program: &accounts[3], amount: data.amount, - max_top_up: None, + fee_payer: None, } .invoke_signed(&[signer])?; diff --git a/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs b/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs index 8488de95de..1d209a3b3f 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs @@ -23,7 +23,7 @@ pub fn process_revoke_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramErro token_account: &accounts[0], owner: &accounts[1], system_program: &accounts[2], - max_top_up: None, + fee_payer: None, } .invoke()?; @@ -58,7 +58,7 @@ pub fn process_revoke_invoke_signed(accounts: &[AccountInfo]) -> Result<(), Prog token_account: &accounts[0], owner: &accounts[1], system_program: &accounts[2], - max_top_up: None, + fee_payer: None, } .invoke_signed(&[signer])?; diff --git a/sdk-tests/sdk-light-token-test/src/approve.rs b/sdk-tests/sdk-light-token-test/src/approve.rs index e9b4f14189..38aab6760d 100644 --- a/sdk-tests/sdk-light-token-test/src/approve.rs +++ b/sdk-tests/sdk-light-token-test/src/approve.rs @@ -32,7 +32,7 @@ pub fn process_approve_invoke( owner: accounts[2].clone(), system_program: accounts[3].clone(), amount: data.amount, - max_top_up: None, + fee_payer: None, } .invoke()?; @@ -70,7 +70,7 @@ pub fn process_approve_invoke_signed( owner: accounts[2].clone(), system_program: accounts[3].clone(), amount: data.amount, - max_top_up: None, + fee_payer: None, } .invoke_signed(&[signer_seeds])?; diff --git a/sdk-tests/sdk-light-token-test/src/burn.rs b/sdk-tests/sdk-light-token-test/src/burn.rs index d1b2bfbf2f..a6dbc0a1ca 100644 --- a/sdk-tests/sdk-light-token-test/src/burn.rs +++ b/sdk-tests/sdk-light-token-test/src/burn.rs @@ -29,7 +29,6 @@ pub fn process_burn_invoke(accounts: &[AccountInfo], amount: u64) -> Result<(), amount, authority: accounts[2].clone(), system_program: accounts[4].clone(), - max_top_up: None, fee_payer: None, } .invoke()?; @@ -68,7 +67,6 @@ pub fn process_burn_invoke_signed( amount, authority: accounts[2].clone(), system_program: accounts[4].clone(), - max_top_up: None, fee_payer: None, } .invoke_signed(&[signer_seeds])?; diff --git a/sdk-tests/sdk-light-token-test/src/ctoken_mint_to.rs b/sdk-tests/sdk-light-token-test/src/ctoken_mint_to.rs index 9b4d10c61b..8a70311472 100644 --- a/sdk-tests/sdk-light-token-test/src/ctoken_mint_to.rs +++ b/sdk-tests/sdk-light-token-test/src/ctoken_mint_to.rs @@ -29,7 +29,6 @@ pub fn process_mint_to_invoke(accounts: &[AccountInfo], amount: u64) -> Result<( amount, authority: accounts[2].clone(), system_program: accounts[3].clone(), - max_top_up: None, fee_payer: None, } .invoke()?; @@ -68,7 +67,6 @@ pub fn process_mint_to_invoke_signed( amount, authority: accounts[2].clone(), system_program: accounts[3].clone(), - max_top_up: None, fee_payer: None, } .invoke_signed(&[signer_seeds])?; diff --git a/sdk-tests/sdk-light-token-test/src/revoke.rs b/sdk-tests/sdk-light-token-test/src/revoke.rs index 99fb65b27d..8cdb0b8d1f 100644 --- a/sdk-tests/sdk-light-token-test/src/revoke.rs +++ b/sdk-tests/sdk-light-token-test/src/revoke.rs @@ -19,7 +19,7 @@ pub fn process_revoke_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramErro token_account: accounts[0].clone(), owner: accounts[1].clone(), system_program: accounts[2].clone(), - max_top_up: None, + fee_payer: None, } .invoke()?; @@ -51,7 +51,7 @@ pub fn process_revoke_invoke_signed(accounts: &[AccountInfo]) -> Result<(), Prog token_account: accounts[0].clone(), owner: accounts[1].clone(), system_program: accounts[2].clone(), - max_top_up: None, + fee_payer: None, } .invoke_signed(&[signer_seeds])?; diff --git a/sdk-tests/sdk-light-token-test/src/transfer.rs b/sdk-tests/sdk-light-token-test/src/transfer.rs index 2ddcf397e5..c9c80c2fa8 100644 --- a/sdk-tests/sdk-light-token-test/src/transfer.rs +++ b/sdk-tests/sdk-light-token-test/src/transfer.rs @@ -36,7 +36,6 @@ pub fn process_transfer_invoke( amount: data.amount, authority: accounts[2].clone(), system_program: accounts[3].clone(), - max_top_up: None, fee_payer: None, } .invoke()?; @@ -79,7 +78,6 @@ pub fn process_transfer_invoke_signed( amount: data.amount, authority: accounts[2].clone(), system_program: accounts[3].clone(), - max_top_up: None, fee_payer: None, }; diff --git a/sdk-tests/sdk-light-token-test/src/transfer_checked.rs b/sdk-tests/sdk-light-token-test/src/transfer_checked.rs index d31f1b2e4f..89382299e1 100644 --- a/sdk-tests/sdk-light-token-test/src/transfer_checked.rs +++ b/sdk-tests/sdk-light-token-test/src/transfer_checked.rs @@ -35,7 +35,6 @@ pub fn process_transfer_checked_invoke( decimals: data.decimals, authority: accounts[3].clone(), system_program: accounts[4].clone(), - max_top_up: None, fee_payer: None, } .invoke()?; @@ -75,7 +74,6 @@ pub fn process_transfer_checked_invoke_signed( decimals: data.decimals, authority: accounts[3].clone(), system_program: accounts[4].clone(), - max_top_up: None, fee_payer: None, }; diff --git a/sdk-tests/sdk-light-token-test/tests/scenario_light_mint.rs b/sdk-tests/sdk-light-token-test/tests/scenario_light_mint.rs index 30851e769d..dfd61c063d 100644 --- a/sdk-tests/sdk-light-token-test/tests/scenario_light_mint.rs +++ b/sdk-tests/sdk-light-token-test/tests/scenario_light_mint.rs @@ -93,7 +93,6 @@ async fn test_mint_to_ctoken_scenario() { destination: ctoken_ata2, amount: transfer_amount, authority: owner1.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() diff --git a/sdk-tests/sdk-light-token-test/tests/scenario_light_mint_compression_only.rs b/sdk-tests/sdk-light-token-test/tests/scenario_light_mint_compression_only.rs index 5651cf95a2..2d0b444712 100644 --- a/sdk-tests/sdk-light-token-test/tests/scenario_light_mint_compression_only.rs +++ b/sdk-tests/sdk-light-token-test/tests/scenario_light_mint_compression_only.rs @@ -98,7 +98,6 @@ async fn test_mint_to_ctoken_scenario_compression_only() { destination: ctoken_ata2, amount: transfer_amount, authority: owner1.pubkey(), - max_top_up: None, fee_payer: None, } .instruction() diff --git a/sdk-tests/sdk-light-token-test/tests/shared.rs b/sdk-tests/sdk-light-token-test/tests/shared.rs index 79f591ffa3..42faa0aac3 100644 --- a/sdk-tests/sdk-light-token-test/tests/shared.rs +++ b/sdk-tests/sdk-light-token-test/tests/shared.rs @@ -123,7 +123,6 @@ pub async fn setup_create_mint( destination: ata_pubkeys[*idx], amount: *amount, authority: mint_authority, - max_top_up: None, fee_payer: None, } .instruction() @@ -244,7 +243,6 @@ pub async fn setup_create_mint_with_freeze_authority( destination: ata_pubkeys[*idx], amount: *amount, authority: mint_authority, - max_top_up: None, fee_payer: None, } .instruction() @@ -384,7 +382,6 @@ pub async fn setup_create_mint_with_compression_only( destination: ata_pubkeys[*idx], amount: *amount, authority: mint_authority, - max_top_up: None, fee_payer: None, } .instruction() From c57dea9cd2df1198dc87da7bb22e74a44d8f715e Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Thu, 19 Feb 2026 17:11:14 +0000 Subject: [PATCH 05/26] rm max top up --- .../token-pinocchio/src/instruction/burn.rs | 1 - .../src/instruction/burn_checked.rs | 1 - .../src/instruction/mint_to.rs | 1 - .../src/instruction/mint_to_checked.rs | 1 - .../src/instruction/transfer.rs | 1 - sdk-libs/token-pinocchio/src/lib.rs | 1 - .../token-sdk/tests/instruction_transfer.rs | 113 ++---------------- sdk-libs/token-sdk/tests/transfer_type.rs | 6 - .../src/amm_test/deposit.rs | 1 - .../src/amm_test/initialize.rs | 1 - .../src/amm_test/withdraw.rs | 1 - .../csdk-anchor-full-derived-test/src/lib.rs | 3 - .../tests/shared.rs | 1 - .../tests/shared/mod.rs | 3 - 14 files changed, 12 insertions(+), 123 deletions(-) diff --git a/sdk-libs/token-pinocchio/src/instruction/burn.rs b/sdk-libs/token-pinocchio/src/instruction/burn.rs index 2bc9f423b0..f0ae4453e7 100644 --- a/sdk-libs/token-pinocchio/src/instruction/burn.rs +++ b/sdk-libs/token-pinocchio/src/instruction/burn.rs @@ -23,7 +23,6 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// amount: 100, /// authority: &ctx.accounts.authority, /// system_program: &ctx.accounts.system_program, -/// max_top_up: None, /// fee_payer: None, /// } /// .invoke()?; diff --git a/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs b/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs index 4793aaf1a8..837925ca32 100644 --- a/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs +++ b/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs @@ -24,7 +24,6 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// decimals: 9, /// authority: &ctx.accounts.authority, /// system_program: &ctx.accounts.system_program, -/// max_top_up: None, /// fee_payer: None, /// } /// .invoke()?; diff --git a/sdk-libs/token-pinocchio/src/instruction/mint_to.rs b/sdk-libs/token-pinocchio/src/instruction/mint_to.rs index 7ca415540a..6982e58937 100644 --- a/sdk-libs/token-pinocchio/src/instruction/mint_to.rs +++ b/sdk-libs/token-pinocchio/src/instruction/mint_to.rs @@ -23,7 +23,6 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// amount: 100, /// authority: &ctx.accounts.authority, /// system_program: &ctx.accounts.system_program, -/// max_top_up: None, /// fee_payer: None, /// } /// .invoke()?; diff --git a/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs b/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs index 38e0dd8688..b0e692e820 100644 --- a/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs +++ b/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs @@ -24,7 +24,6 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// decimals: 9, /// authority: &ctx.accounts.authority, /// system_program: &ctx.accounts.system_program, -/// max_top_up: None, /// fee_payer: None, /// } /// .invoke()?; diff --git a/sdk-libs/token-pinocchio/src/instruction/transfer.rs b/sdk-libs/token-pinocchio/src/instruction/transfer.rs index b27b3ff013..b9ab960a6e 100644 --- a/sdk-libs/token-pinocchio/src/instruction/transfer.rs +++ b/sdk-libs/token-pinocchio/src/instruction/transfer.rs @@ -23,7 +23,6 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// amount: 100, /// authority: &ctx.accounts.authority, /// system_program: &ctx.accounts.system_program, -/// max_top_up: None, /// fee_payer: None, /// } /// .invoke()?; diff --git a/sdk-libs/token-pinocchio/src/lib.rs b/sdk-libs/token-pinocchio/src/lib.rs index 8b772649a1..452e3641d6 100644 --- a/sdk-libs/token-pinocchio/src/lib.rs +++ b/sdk-libs/token-pinocchio/src/lib.rs @@ -35,7 +35,6 @@ //! amount: 100, //! authority: &ctx.accounts.authority, //! system_program: &ctx.accounts.system_program, -//! max_top_up: None, //! fee_payer: None, //! } //! .invoke()?; diff --git a/sdk-libs/token-sdk/tests/instruction_transfer.rs b/sdk-libs/token-sdk/tests/instruction_transfer.rs index 7752bb14fe..ff2859068a 100644 --- a/sdk-libs/token-sdk/tests/instruction_transfer.rs +++ b/sdk-libs/token-sdk/tests/instruction_transfer.rs @@ -2,8 +2,9 @@ use light_token::instruction::{Transfer, LIGHT_TOKEN_PROGRAM_ID}; use solana_instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; -/// Test Transfer instruction with no max_top_up or fee_payer. -/// Authority is readonly signer since it doesn't need to pay for top-ups. +/// Test Transfer instruction without fee_payer. +/// Authority is writable signer since it pays for top-ups. +/// Data always includes max_top_up as u16::MAX. #[test] fn test_transfer_basic() { let source = Pubkey::new_from_array([1u8; 32]); @@ -15,58 +16,14 @@ fn test_transfer_basic() { destination, amount: 100, authority, - max_top_up: None, fee_payer: None, } .instruction() .expect("Failed to create instruction"); // Hardcoded expected instruction - // - authority is readonly (no max_top_up) - // - data: discriminator (3) + amount (100 as le u64) = 9 bytes - let expected = Instruction { - program_id: LIGHT_TOKEN_PROGRAM_ID, - accounts: vec![ - AccountMeta::new(source, false), // source: writable, not signer - AccountMeta::new(destination, false), // destination: writable, not signer - AccountMeta::new_readonly(authority, true), // authority: readonly, signer - AccountMeta::new_readonly(Pubkey::default(), false), // system_program: readonly, not signer - ], - data: vec![ - 3u8, // Transfer discriminator - 100, 0, 0, 0, 0, 0, 0, 0, // amount: 100 as little-endian u64 - ], - }; - - assert_eq!( - instruction, expected, - "Transfer instruction should match expected" - ); -} - -/// Test Transfer instruction with max_top_up set (no fee_payer). -/// Authority becomes writable to pay for potential top-ups. -/// Data includes max_top_up as 2 extra bytes. -#[test] -fn test_transfer_with_max_top_up() { - let source = Pubkey::new_from_array([1u8; 32]); - let destination = Pubkey::new_from_array([2u8; 32]); - let authority = Pubkey::new_from_array([3u8; 32]); - - let instruction = Transfer { - source, - destination, - amount: 100, - authority, - max_top_up: Some(500), - fee_payer: None, - } - .instruction() - .expect("Failed to create instruction"); - - // Hardcoded expected instruction - // - authority is writable (max_top_up set, no fee_payer -> authority pays) - // - data: discriminator (3) + amount (8 bytes) + max_top_up (2 bytes) = 11 bytes + // - authority is writable (no fee_payer -> authority pays for top-ups) + // - data: discriminator (3) + amount (8 bytes) + max_top_up u16::MAX (2 bytes) = 11 bytes let expected = Instruction { program_id: LIGHT_TOKEN_PROGRAM_ID, accounts: vec![ @@ -78,18 +35,19 @@ fn test_transfer_with_max_top_up() { data: vec![ 3u8, // Transfer discriminator 100, 0, 0, 0, 0, 0, 0, 0, // amount: 100 as little-endian u64 - 244, 1, // max_top_up: 500 as little-endian u16 + 255, 255, // max_top_up: u16::MAX as little-endian u16 ], }; assert_eq!( instruction, expected, - "Transfer instruction with max_top_up should match expected" + "Transfer instruction should match expected" ); } -/// Test Transfer instruction with fee_payer set (no max_top_up). -/// Fee_payer is added as 5th account. Authority remains readonly. +/// Test Transfer instruction with fee_payer set. +/// Fee_payer is added as 5th account. Authority is readonly. +/// Data always includes max_top_up as u16::MAX. #[test] fn test_transfer_with_fee_payer() { let source = Pubkey::new_from_array([1u8; 32]); @@ -102,7 +60,6 @@ fn test_transfer_with_fee_payer() { destination, amount: 100, authority, - max_top_up: None, fee_payer: Some(fee_payer), } .instruction() @@ -111,7 +68,7 @@ fn test_transfer_with_fee_payer() { // Hardcoded expected instruction // - authority is readonly (fee_payer pays instead) // - fee_payer is 5th account: writable, signer - // - data: discriminator (3) + amount (8 bytes) = 9 bytes (no max_top_up) + // - data: discriminator (3) + amount (8 bytes) + max_top_up u16::MAX (2 bytes) = 11 bytes let expected = Instruction { program_id: LIGHT_TOKEN_PROGRAM_ID, accounts: vec![ @@ -124,6 +81,7 @@ fn test_transfer_with_fee_payer() { data: vec![ 3u8, // Transfer discriminator 100, 0, 0, 0, 0, 0, 0, 0, // amount: 100 as little-endian u64 + 255, 255, // max_top_up: u16::MAX as little-endian u16 ], }; @@ -132,50 +90,3 @@ fn test_transfer_with_fee_payer() { "Transfer instruction with fee_payer should match expected" ); } - -/// Test Transfer instruction with both max_top_up and fee_payer set. -/// Authority is readonly (fee_payer pays for top-ups). -/// Data includes max_top_up. Fee_payer is 5th account. -#[test] -fn test_transfer_with_max_top_up_and_fee_payer() { - let source = Pubkey::new_from_array([1u8; 32]); - let destination = Pubkey::new_from_array([2u8; 32]); - let authority = Pubkey::new_from_array([3u8; 32]); - let fee_payer = Pubkey::new_from_array([4u8; 32]); - - let instruction = Transfer { - source, - destination, - amount: 100, - authority, - max_top_up: Some(500), - fee_payer: Some(fee_payer), - } - .instruction() - .expect("Failed to create instruction"); - - // Hardcoded expected instruction - // - authority is readonly (fee_payer pays instead, even with max_top_up) - // - fee_payer is 5th account: writable, signer - // - data: discriminator (3) + amount (8 bytes) + max_top_up (2 bytes) = 11 bytes - let expected = Instruction { - program_id: LIGHT_TOKEN_PROGRAM_ID, - accounts: vec![ - AccountMeta::new(source, false), // source: writable, not signer - AccountMeta::new(destination, false), // destination: writable, not signer - AccountMeta::new_readonly(authority, true), // authority: readonly, signer - AccountMeta::new_readonly(Pubkey::default(), false), // system_program: readonly, not signer - AccountMeta::new(fee_payer, true), // fee_payer: writable, signer - ], - data: vec![ - 3u8, // Transfer discriminator - 100, 0, 0, 0, 0, 0, 0, 0, // amount: 100 as little-endian u64 - 244, 1, // max_top_up: 500 as little-endian u16 - ], - }; - - assert_eq!( - instruction, expected, - "Transfer instruction with max_top_up and fee_payer should match expected" - ); -} diff --git a/sdk-libs/token-sdk/tests/transfer_type.rs b/sdk-libs/token-sdk/tests/transfer_type.rs index d38238f773..47e7db1b79 100644 --- a/sdk-libs/token-sdk/tests/transfer_type.rs +++ b/sdk-libs/token-sdk/tests/transfer_type.rs @@ -112,7 +112,6 @@ fn test_transfer_interface_light_to_light_no_spl_interface() { authority, payer, spl_interface: None, // No SPL interface needed - max_top_up: None, source_owner: LIGHT_TOKEN_PROGRAM_ID, destination_owner: LIGHT_TOKEN_PROGRAM_ID, }; @@ -147,7 +146,6 @@ fn test_transfer_interface_light_to_spl_requires_interface() { authority, payer, spl_interface: None, // Missing required interface - max_top_up: None, source_owner: LIGHT_TOKEN_PROGRAM_ID, destination_owner: SPL_TOKEN_PROGRAM_ID, }; @@ -177,7 +175,6 @@ fn test_transfer_interface_spl_to_light_requires_interface() { authority, payer, spl_interface: None, // Missing required interface - max_top_up: None, source_owner: SPL_TOKEN_PROGRAM_ID, destination_owner: LIGHT_TOKEN_PROGRAM_ID, }; @@ -213,7 +210,6 @@ fn test_transfer_interface_light_to_spl_with_interface() { spl_interface_pda, spl_interface_pda_bump: 255, }), - max_top_up: None, source_owner: LIGHT_TOKEN_PROGRAM_ID, destination_owner: SPL_TOKEN_PROGRAM_ID, }; @@ -243,7 +239,6 @@ fn test_transfer_interface_spl_to_spl_requires_interface() { authority, payer, spl_interface: None, // Missing interface - max_top_up: None, source_owner: SPL_TOKEN_PROGRAM_ID, destination_owner: SPL_TOKEN_PROGRAM_ID, }; @@ -280,7 +275,6 @@ fn test_transfer_interface_spl_program_mismatch() { spl_interface_pda, spl_interface_pda_bump: 255, }), - max_top_up: None, source_owner: SPL_TOKEN_PROGRAM_ID, destination_owner: SPL_TOKEN_2022_PROGRAM_ID, }; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/deposit.rs b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/deposit.rs index 9ad42ce187..2ef54933d5 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/deposit.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/deposit.rs @@ -79,7 +79,6 @@ pub fn process_deposit(ctx: Context, lp_token_amount: u64) -> Result<() amount: lp_token_amount, authority: ctx.accounts.authority.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), - max_top_up: None, fee_payer: None, } .invoke_signed(&[&[AUTH_SEED.as_bytes(), &[auth_bump]]])?; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs index 979530e4b6..4864f83e3b 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs @@ -254,7 +254,6 @@ pub fn process_initialize_pool<'info>( amount: lp_amount, authority: ctx.accounts.authority.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), - max_top_up: None, fee_payer: None, } .invoke_signed(&[&[AUTH_SEED.as_bytes(), &[ctx.bumps.authority]]])?; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/withdraw.rs b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/withdraw.rs index d9472349e0..5416c8abdb 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/withdraw.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/withdraw.rs @@ -76,7 +76,6 @@ pub fn process_withdraw(ctx: Context, lp_token_amount: u64) -> Result< amount: lp_token_amount, authority: ctx.accounts.owner.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), - max_top_up: None, fee_payer: None, } .invoke()?; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs index f9387ac017..1282ebb68b 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -385,7 +385,6 @@ pub mod csdk_anchor_full_derived_test { amount: params.vault_mint_amount, authority: ctx.accounts.mint_authority.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), - max_top_up: None, fee_payer: None, } .invoke()?; @@ -398,7 +397,6 @@ pub mod csdk_anchor_full_derived_test { amount: params.user_ata_mint_amount, authority: ctx.accounts.mint_authority.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), - max_top_up: None, fee_payer: None, } .invoke()?; @@ -1603,7 +1601,6 @@ pub mod csdk_anchor_full_derived_test { amount: params.mint_amount, authority: ctx.accounts.mint_authority.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), - max_top_up: None, fee_payer: None, } .invoke()?; diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/shared.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/shared.rs index 5ccd85b84f..db52561bec 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/shared.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/shared.rs @@ -341,7 +341,6 @@ pub async fn setup_create_mint( destination: ata_pubkeys[*idx], amount: *amount, authority: mint_authority, - max_top_up: None, fee_payer: None, } .instruction() diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/shared/mod.rs b/sdk-tests/sdk-light-token-pinocchio/tests/shared/mod.rs index 7e35bdd140..8ad9cb03df 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/shared/mod.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/shared/mod.rs @@ -127,7 +127,6 @@ pub async fn setup_create_mint( destination: ata_pubkeys[*idx], amount: *amount, authority: mint_authority, - max_top_up: None, fee_payer: None, } .instruction() @@ -248,7 +247,6 @@ pub async fn setup_create_mint_with_freeze_authority( destination: ata_pubkeys[*idx], amount: *amount, authority: mint_authority, - max_top_up: None, fee_payer: None, } .instruction() @@ -388,7 +386,6 @@ pub async fn setup_create_mint_with_compression_only( destination: ata_pubkeys[*idx], amount: *amount, authority: mint_authority, - max_top_up: None, fee_payer: None, } .instruction() From 4fa68c3d9b54d3d3faa1fee8aabb9d551e0ba1f8 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Thu, 19 Feb 2026 17:23:26 +0000 Subject: [PATCH 06/26] rm max top up --- .../tests/light_token/approve_revoke.rs | 2 - .../tests/light_token/shared.rs | 4 - sdk-libs/token-client/src/actions/approve.rs | 2 - sdk-libs/token-client/src/actions/revoke.rs | 2 - .../src/instruction/approve.rs | 78 +++++------------ .../token-pinocchio/src/instruction/revoke.rs | 65 ++++---------- sdk-libs/token-sdk/src/instruction/approve.rs | 85 +++++-------------- sdk-libs/token-sdk/src/instruction/revoke.rs | 55 +++--------- .../sdk-light-token-pinocchio/src/approve.rs | 2 - .../sdk-light-token-pinocchio/src/revoke.rs | 2 - sdk-tests/sdk-light-token-test/src/approve.rs | 2 - sdk-tests/sdk-light-token-test/src/revoke.rs | 2 - 12 files changed, 70 insertions(+), 231 deletions(-) diff --git a/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs b/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs index f3270fea0a..b2bea672cf 100644 --- a/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs +++ b/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs @@ -368,7 +368,6 @@ async fn test_approve_revoke_compressible() -> Result<(), RpcError> { delegate: delegate.pubkey(), owner: owner.pubkey(), amount: approve_amount, - fee_payer: None, } .instruction() .map_err(|e| { @@ -393,7 +392,6 @@ async fn test_approve_revoke_compressible() -> Result<(), RpcError> { let revoke_ix = Revoke { token_account: account_pubkey, owner: owner.pubkey(), - fee_payer: None, } .instruction() .map_err(|e| RpcError::AssertRpcError(format!("Failed to create revoke instruction: {}", e)))?; diff --git a/program-tests/compressed-token-test/tests/light_token/shared.rs b/program-tests/compressed-token-test/tests/light_token/shared.rs index 3e5743105a..bcd00879bb 100644 --- a/program-tests/compressed-token-test/tests/light_token/shared.rs +++ b/program-tests/compressed-token-test/tests/light_token/shared.rs @@ -903,7 +903,6 @@ pub async fn approve_and_assert( delegate, owner: context.owner_keypair.pubkey(), amount, - fee_payer: None, } .instruction() .unwrap(); @@ -946,7 +945,6 @@ pub async fn approve_and_assert_fails( delegate, owner: authority.pubkey(), amount, - fee_payer: None, } .instruction() .unwrap(); @@ -971,7 +969,6 @@ pub async fn revoke_and_assert(context: &mut AccountTestContext, name: &str) { let revoke_ix = Revoke { token_account: context.token_account_keypair.pubkey(), owner: context.owner_keypair.pubkey(), - fee_payer: None, } .instruction() .unwrap(); @@ -1003,7 +1000,6 @@ pub async fn revoke_and_assert_fails( let instruction = Revoke { token_account, owner: authority.pubkey(), - fee_payer: None, } .instruction() .unwrap(); diff --git a/sdk-libs/token-client/src/actions/approve.rs b/sdk-libs/token-client/src/actions/approve.rs index 1bf7d4b938..547acb1b1d 100644 --- a/sdk-libs/token-client/src/actions/approve.rs +++ b/sdk-libs/token-client/src/actions/approve.rs @@ -79,7 +79,6 @@ impl Approve { delegate: self.delegate, owner: owner_pubkey, amount: self.amount, - fee_payer: None, } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; @@ -122,7 +121,6 @@ impl Approve { delegate: self.delegate, owner: owner.pubkey(), amount: self.amount, - fee_payer: None, } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; diff --git a/sdk-libs/token-client/src/actions/revoke.rs b/sdk-libs/token-client/src/actions/revoke.rs index 6e7a20b1f4..803b4516ad 100644 --- a/sdk-libs/token-client/src/actions/revoke.rs +++ b/sdk-libs/token-client/src/actions/revoke.rs @@ -69,7 +69,6 @@ impl Revoke { let ix = RevokeInstruction { token_account: self.token_account, owner: owner_pubkey, - fee_payer: None, } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; @@ -96,7 +95,6 @@ impl Revoke { let ix = RevokeInstruction { token_account: self.token_account, owner: owner.pubkey(), - fee_payer: None, } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; diff --git a/sdk-libs/token-pinocchio/src/instruction/approve.rs b/sdk-libs/token-pinocchio/src/instruction/approve.rs index 999096778f..af6f821271 100644 --- a/sdk-libs/token-pinocchio/src/instruction/approve.rs +++ b/sdk-libs/token-pinocchio/src/instruction/approve.rs @@ -23,7 +23,6 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// owner: &ctx.accounts.owner, /// system_program: &ctx.accounts.system_program, /// amount: 100, -/// fee_payer: None, /// } /// .invoke()?; /// ``` @@ -33,8 +32,6 @@ pub struct ApproveCpi<'info> { pub owner: &'info AccountInfo, pub system_program: &'info AccountInfo, pub amount: u64, - /// Optional fee payer for rent top-ups. If not provided, owner pays. - pub fee_payer: Option<&'info AccountInfo>, } impl<'info> ApproveCpi<'info> { @@ -43,68 +40,37 @@ impl<'info> ApproveCpi<'info> { } pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { - // Build instruction data: discriminator(1) + amount(8) + max_top_up(2) - let mut data = [0u8; 11]; + // Build instruction data: discriminator(1) + amount(8) + let mut data = [0u8; 9]; data[0] = 4u8; // Approve discriminator data[1..9].copy_from_slice(&self.amount.to_le_bytes()); - data[9..11].copy_from_slice(&u16::MAX.to_le_bytes()); let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); - if let Some(fee_payer) = self.fee_payer { - let account_metas = [ - AccountMeta::writable(self.token_account.key()), - AccountMeta::readonly(self.delegate.key()), - AccountMeta::readonly_signer(self.owner.key()), - AccountMeta::readonly(self.system_program.key()), - AccountMeta::writable_signer(fee_payer.key()), - ]; + let account_metas = [ + AccountMeta::writable(self.token_account.key()), + AccountMeta::readonly(self.delegate.key()), + AccountMeta::writable_signer(self.owner.key()), + AccountMeta::readonly(self.system_program.key()), + ]; - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data, - }; + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data, + }; - let account_infos = [ - self.token_account, - self.delegate, - self.owner, - self.system_program, - fee_payer, - ]; + let account_infos = [ + self.token_account, + self.delegate, + self.owner, + self.system_program, + ]; - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) - } else { - slice_invoke_signed(&instruction, &account_infos, signers) - } + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) } else { - let account_metas = [ - AccountMeta::writable(self.token_account.key()), - AccountMeta::readonly(self.delegate.key()), - AccountMeta::writable_signer(self.owner.key()), - AccountMeta::readonly(self.system_program.key()), - ]; - - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data, - }; - - let account_infos = [ - self.token_account, - self.delegate, - self.owner, - self.system_program, - ]; - - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) - } else { - slice_invoke_signed(&instruction, &account_infos, signers) - } + slice_invoke_signed(&instruction, &account_infos, signers) } } } diff --git a/sdk-libs/token-pinocchio/src/instruction/revoke.rs b/sdk-libs/token-pinocchio/src/instruction/revoke.rs index 16faed6127..1cbca61334 100644 --- a/sdk-libs/token-pinocchio/src/instruction/revoke.rs +++ b/sdk-libs/token-pinocchio/src/instruction/revoke.rs @@ -21,7 +21,6 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// token_account: &ctx.accounts.token_account, /// owner: &ctx.accounts.owner, /// system_program: &ctx.accounts.system_program, -/// fee_payer: None, /// } /// .invoke()?; /// ``` @@ -29,8 +28,6 @@ pub struct RevokeCpi<'info> { pub token_account: &'info AccountInfo, pub owner: &'info AccountInfo, pub system_program: &'info AccountInfo, - /// Optional fee payer for rent top-ups. If not provided, owner pays. - pub fee_payer: Option<&'info AccountInfo>, } impl<'info> RevokeCpi<'info> { @@ -39,59 +36,29 @@ impl<'info> RevokeCpi<'info> { } pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { - // Build instruction data: discriminator(1) + max_top_up(2) - let mut data = [0u8; 3]; - data[0] = 5u8; // Revoke discriminator - data[1..3].copy_from_slice(&u16::MAX.to_le_bytes()); + // Build instruction data: discriminator(1) only + let data = [5u8]; // Revoke discriminator let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); - if let Some(fee_payer) = self.fee_payer { - let account_metas = [ - AccountMeta::writable(self.token_account.key()), - AccountMeta::readonly_signer(self.owner.key()), - AccountMeta::readonly(self.system_program.key()), - AccountMeta::writable_signer(fee_payer.key()), - ]; + let account_metas = [ + AccountMeta::writable(self.token_account.key()), + AccountMeta::writable_signer(self.owner.key()), + AccountMeta::readonly(self.system_program.key()), + ]; - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data, - }; + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data, + }; - let account_infos = [ - self.token_account, - self.owner, - self.system_program, - fee_payer, - ]; + let account_infos = [self.token_account, self.owner, self.system_program]; - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) - } else { - slice_invoke_signed(&instruction, &account_infos, signers) - } + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) } else { - let account_metas = [ - AccountMeta::writable(self.token_account.key()), - AccountMeta::writable_signer(self.owner.key()), - AccountMeta::readonly(self.system_program.key()), - ]; - - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data, - }; - - let account_infos = [self.token_account, self.owner, self.system_program]; - - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) - } else { - slice_invoke_signed(&instruction, &account_infos, signers) - } + slice_invoke_signed(&instruction, &account_infos, signers) } } } diff --git a/sdk-libs/token-sdk/src/instruction/approve.rs b/sdk-libs/token-sdk/src/instruction/approve.rs index 11f20c340d..b1c6a8cff1 100644 --- a/sdk-libs/token-sdk/src/instruction/approve.rs +++ b/sdk-libs/token-sdk/src/instruction/approve.rs @@ -17,7 +17,6 @@ use solana_pubkey::Pubkey; /// delegate, /// owner, /// amount: 100, -/// fee_payer: None, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` @@ -26,12 +25,10 @@ pub struct Approve { pub token_account: Pubkey, /// Delegate to approve pub delegate: Pubkey, - /// Owner of the Light Token account (signer) + /// Owner of the Light Token account (signer, payer for top-up) pub owner: Pubkey, /// Amount of tokens to delegate pub amount: u64, - /// Optional fee payer for rent top-ups. If not provided, owner pays. - pub fee_payer: Option, } /// # Approve Light Token via CPI: @@ -48,7 +45,6 @@ pub struct Approve { /// owner, /// system_program, /// amount: 100, -/// fee_payer: None, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -59,8 +55,6 @@ pub struct ApproveCpi<'info> { pub owner: AccountInfo<'info>, pub system_program: AccountInfo<'info>, pub amount: u64, - /// Optional fee payer for rent top-ups. If not provided, owner pays. - pub fee_payer: Option>, } impl<'info> ApproveCpi<'info> { @@ -70,46 +64,24 @@ impl<'info> ApproveCpi<'info> { pub fn invoke(self) -> Result<(), ProgramError> { let instruction = Approve::from(&self).instruction()?; - if let Some(fee_payer) = self.fee_payer { - let account_infos = [ - self.token_account, - self.delegate, - self.owner, - self.system_program, - fee_payer, - ]; - invoke(&instruction, &account_infos) - } else { - let account_infos = [ - self.token_account, - self.delegate, - self.owner, - self.system_program, - ]; - invoke(&instruction, &account_infos) - } + let account_infos = [ + self.token_account, + self.delegate, + self.owner, + self.system_program, + ]; + invoke(&instruction, &account_infos) } pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { let instruction = Approve::from(&self).instruction()?; - if let Some(fee_payer) = self.fee_payer { - let account_infos = [ - self.token_account, - self.delegate, - self.owner, - self.system_program, - fee_payer, - ]; - invoke_signed(&instruction, &account_infos, signer_seeds) - } else { - let account_infos = [ - self.token_account, - self.delegate, - self.owner, - self.system_program, - ]; - invoke_signed(&instruction, &account_infos, signer_seeds) - } + let account_infos = [ + self.token_account, + self.delegate, + self.owner, + self.system_program, + ]; + invoke_signed(&instruction, &account_infos, signer_seeds) } } @@ -120,7 +92,6 @@ impl<'info> From<&ApproveCpi<'info>> for Approve { delegate: *cpi.delegate.key, owner: *cpi.owner.key, amount: cpi.amount, - fee_payer: cpi.fee_payer.as_ref().map(|a| *a.key), } } } @@ -129,29 +100,15 @@ impl Approve { pub fn instruction(self) -> Result { let mut data = vec![4u8]; // CTokenApprove discriminator data.extend_from_slice(&self.amount.to_le_bytes()); - data.extend_from_slice(&u16::MAX.to_le_bytes()); - - // Owner is writable only when no fee_payer (owner pays for top-ups) - let owner_meta = if self.fee_payer.is_none() { - AccountMeta::new(self.owner, true) - } else { - AccountMeta::new_readonly(self.owner, true) - }; - - let mut accounts = vec![ - AccountMeta::new(self.token_account, false), - AccountMeta::new_readonly(self.delegate, false), - owner_meta, - AccountMeta::new_readonly(Pubkey::default(), false), - ]; - - if let Some(fee_payer) = self.fee_payer { - accounts.push(AccountMeta::new(fee_payer, true)); - } Ok(Instruction { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), - accounts, + accounts: vec![ + AccountMeta::new(self.token_account, false), + AccountMeta::new_readonly(self.delegate, false), + AccountMeta::new(self.owner, true), + AccountMeta::new_readonly(Pubkey::default(), false), + ], data, }) } diff --git a/sdk-libs/token-sdk/src/instruction/revoke.rs b/sdk-libs/token-sdk/src/instruction/revoke.rs index bba21f3365..d746dbe850 100644 --- a/sdk-libs/token-sdk/src/instruction/revoke.rs +++ b/sdk-libs/token-sdk/src/instruction/revoke.rs @@ -14,17 +14,14 @@ use solana_pubkey::Pubkey; /// let instruction = Revoke { /// token_account, /// owner, -/// fee_payer: None, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` pub struct Revoke { /// Light Token account to revoke delegation for pub token_account: Pubkey, - /// Owner of the Light Token account (signer) + /// Owner of the Light Token account (signer, payer for top-up) pub owner: Pubkey, - /// Optional fee payer for rent top-ups. If not provided, owner pays. - pub fee_payer: Option, } /// # Revoke Light Token via CPI: @@ -38,7 +35,6 @@ pub struct Revoke { /// token_account, /// owner, /// system_program, -/// fee_payer: None, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -47,8 +43,6 @@ pub struct RevokeCpi<'info> { pub token_account: AccountInfo<'info>, pub owner: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Optional fee payer for rent top-ups. If not provided, owner pays. - pub fee_payer: Option>, } impl<'info> RevokeCpi<'info> { @@ -58,24 +52,14 @@ impl<'info> RevokeCpi<'info> { pub fn invoke(self) -> Result<(), ProgramError> { let instruction = Revoke::from(&self).instruction()?; - if let Some(fee_payer) = self.fee_payer { - let account_infos = [self.token_account, self.owner, self.system_program, fee_payer]; - invoke(&instruction, &account_infos) - } else { - let account_infos = [self.token_account, self.owner, self.system_program]; - invoke(&instruction, &account_infos) - } + let account_infos = [self.token_account, self.owner, self.system_program]; + invoke(&instruction, &account_infos) } pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { let instruction = Revoke::from(&self).instruction()?; - if let Some(fee_payer) = self.fee_payer { - let account_infos = [self.token_account, self.owner, self.system_program, fee_payer]; - invoke_signed(&instruction, &account_infos, signer_seeds) - } else { - let account_infos = [self.token_account, self.owner, self.system_program]; - invoke_signed(&instruction, &account_infos, signer_seeds) - } + let account_infos = [self.token_account, self.owner, self.system_program]; + invoke_signed(&instruction, &account_infos, signer_seeds) } } @@ -84,37 +68,20 @@ impl<'info> From<&RevokeCpi<'info>> for Revoke { Self { token_account: *cpi.token_account.key, owner: *cpi.owner.key, - fee_payer: cpi.fee_payer.as_ref().map(|a| *a.key), } } } impl Revoke { pub fn instruction(self) -> Result { - let mut data = vec![5u8]; // CTokenRevoke discriminator - data.extend_from_slice(&u16::MAX.to_le_bytes()); - - // Owner is writable only when no fee_payer (owner pays for top-ups) - let owner_meta = if self.fee_payer.is_none() { - AccountMeta::new(self.owner, true) - } else { - AccountMeta::new_readonly(self.owner, true) - }; - - let mut accounts = vec![ - AccountMeta::new(self.token_account, false), - owner_meta, - AccountMeta::new_readonly(Pubkey::default(), false), - ]; - - if let Some(fee_payer) = self.fee_payer { - accounts.push(AccountMeta::new(fee_payer, true)); - } - Ok(Instruction { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), - accounts, - data, + accounts: vec![ + AccountMeta::new(self.token_account, false), + AccountMeta::new(self.owner, true), + AccountMeta::new_readonly(Pubkey::default(), false), + ], + data: vec![5u8], // CTokenRevoke discriminator }) } } diff --git a/sdk-tests/sdk-light-token-pinocchio/src/approve.rs b/sdk-tests/sdk-light-token-pinocchio/src/approve.rs index d97e3d47bd..3aacf4d3ed 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/approve.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/approve.rs @@ -36,7 +36,6 @@ pub fn process_approve_invoke( owner: &accounts[2], system_program: &accounts[3], amount: data.amount, - fee_payer: None, } .invoke()?; @@ -75,7 +74,6 @@ pub fn process_approve_invoke_signed( owner: &accounts[2], system_program: &accounts[3], amount: data.amount, - fee_payer: None, } .invoke_signed(&[signer])?; diff --git a/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs b/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs index 1d209a3b3f..a1fb1508f9 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs @@ -23,7 +23,6 @@ pub fn process_revoke_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramErro token_account: &accounts[0], owner: &accounts[1], system_program: &accounts[2], - fee_payer: None, } .invoke()?; @@ -58,7 +57,6 @@ pub fn process_revoke_invoke_signed(accounts: &[AccountInfo]) -> Result<(), Prog token_account: &accounts[0], owner: &accounts[1], system_program: &accounts[2], - fee_payer: None, } .invoke_signed(&[signer])?; diff --git a/sdk-tests/sdk-light-token-test/src/approve.rs b/sdk-tests/sdk-light-token-test/src/approve.rs index 38aab6760d..0dda096b88 100644 --- a/sdk-tests/sdk-light-token-test/src/approve.rs +++ b/sdk-tests/sdk-light-token-test/src/approve.rs @@ -32,7 +32,6 @@ pub fn process_approve_invoke( owner: accounts[2].clone(), system_program: accounts[3].clone(), amount: data.amount, - fee_payer: None, } .invoke()?; @@ -70,7 +69,6 @@ pub fn process_approve_invoke_signed( owner: accounts[2].clone(), system_program: accounts[3].clone(), amount: data.amount, - fee_payer: None, } .invoke_signed(&[signer_seeds])?; diff --git a/sdk-tests/sdk-light-token-test/src/revoke.rs b/sdk-tests/sdk-light-token-test/src/revoke.rs index 8cdb0b8d1f..ff55fbecce 100644 --- a/sdk-tests/sdk-light-token-test/src/revoke.rs +++ b/sdk-tests/sdk-light-token-test/src/revoke.rs @@ -19,7 +19,6 @@ pub fn process_revoke_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramErro token_account: accounts[0].clone(), owner: accounts[1].clone(), system_program: accounts[2].clone(), - fee_payer: None, } .invoke()?; @@ -51,7 +50,6 @@ pub fn process_revoke_invoke_signed(accounts: &[AccountInfo]) -> Result<(), Prog token_account: accounts[0].clone(), owner: accounts[1].clone(), system_program: accounts[2].clone(), - fee_payer: None, } .invoke_signed(&[signer_seeds])?; From 5eaa46de8eeccc3c7d9435e1c37a9fdc9e345d1c Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Thu, 19 Feb 2026 17:44:38 +0000 Subject: [PATCH 07/26] fix(tests): mark PDA authority writable in invoke_signed tests Authority must be writable when no fee_payer is set since it pays for compressible account rent top-ups. --- sdk-tests/sdk-light-token-test/tests/test_burn.rs | 2 +- sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs | 2 +- sdk-tests/sdk-light-token-test/tests/test_transfer.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk-tests/sdk-light-token-test/tests/test_burn.rs b/sdk-tests/sdk-light-token-test/tests/test_burn.rs index 1eb3a70b2d..e09d2b3f63 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_burn.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_burn.rs @@ -119,7 +119,7 @@ async fn test_burn_invoke_signed() { accounts: vec![ AccountMeta::new(ata, false), // source AccountMeta::new(mint_pda, false), // mint - AccountMeta::new_readonly(pda_owner, false), // PDA authority (program signs) + AccountMeta::new(pda_owner, false), // PDA authority (writable, pays for top-ups) AccountMeta::new_readonly(light_token_program, false), // light_token_program AccountMeta::new_readonly(Pubkey::default(), false), // system_program ], diff --git a/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs b/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs index 69b6e822e7..f61da04471 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs @@ -169,7 +169,7 @@ async fn test_ctoken_mint_to_invoke_signed() { AccountMeta::new_readonly(compressed_token_program_id, false), // [0] AccountMeta::new_readonly(default_pubkeys.light_system_program, false), // [1] AccountMeta::new_readonly(mint_signer_pda, false), // [2] mint_signer PDA - AccountMeta::new_readonly(pda_mint_authority, false), // [3] authority PDA + AccountMeta::new(pda_mint_authority, false), // [3] authority PDA (writable, pays for top-ups) AccountMeta::new_readonly(compressible_config, false), // [4] compressible_config AccountMeta::new(mint_pda, false), // [5] mint AccountMeta::new(rent_sponsor, false), // [6] rent_sponsor diff --git a/sdk-tests/sdk-light-token-test/tests/test_transfer.rs b/sdk-tests/sdk-light-token-test/tests/test_transfer.rs index 011494801c..98038f1566 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_transfer.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_transfer.rs @@ -107,7 +107,7 @@ async fn test_ctoken_transfer_invoke_signed() { accounts: vec![ AccountMeta::new(source_ata, false), AccountMeta::new(dest_ata, false), - AccountMeta::new_readonly(pda_owner, false), // PDA authority, not signer + AccountMeta::new(pda_owner, false), // PDA authority (writable, pays for top-ups) AccountMeta::new_readonly(Pubkey::default(), false), // system_program AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), ], From 0b2addabf51744f32f350913f6dc0b02f5b18ce1 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Thu, 19 Feb 2026 18:01:19 +0000 Subject: [PATCH 08/26] fix: restore accidentally deleted anchor build artifacts --- .../target/idl/sdk_anchor_test.json | 726 +++++++++++++++++ .../target/types/sdk_anchor_test.ts | 732 ++++++++++++++++++ 2 files changed, 1458 insertions(+) create mode 100644 sdk-tests/sdk-anchor-test/target/idl/sdk_anchor_test.json create mode 100644 sdk-tests/sdk-anchor-test/target/types/sdk_anchor_test.ts diff --git a/sdk-tests/sdk-anchor-test/target/idl/sdk_anchor_test.json b/sdk-tests/sdk-anchor-test/target/idl/sdk_anchor_test.json new file mode 100644 index 0000000000..0e4e427c40 --- /dev/null +++ b/sdk-tests/sdk-anchor-test/target/idl/sdk_anchor_test.json @@ -0,0 +1,726 @@ +{ + "address": "2tzfijPBGbrR5PboyFUFKzfEoLTwdDSHUjANCw929wyt", + "metadata": { + "name": "sdk_anchor_test", + "version": "0.7.0", + "spec": "0.1.0", + "description": "Test program for Light SDK and Light Macros" + }, + "instructions": [ + { + "name": "close_compressed_account", + "discriminator": [ + 55, + 108, + 99, + 108, + 119, + 228, + 247, + 203 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "proof", + "type": { + "defined": { + "name": "ValidityProof" + } + } + }, + { + "name": "my_compressed_account", + "type": { + "defined": { + "name": "MyCompressedAccount" + } + } + }, + { + "name": "account_meta", + "type": { + "defined": { + "name": "CompressedAccountMeta" + } + } + } + ] + }, + { + "name": "close_compressed_account_permanent", + "discriminator": [ + 117, + 145, + 242, + 98, + 46, + 187, + 118, + 125 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "proof", + "type": { + "defined": { + "name": "ValidityProof" + } + } + }, + { + "name": "account_meta", + "type": { + "defined": { + "name": "CompressedAccountMetaBurn" + } + } + } + ] + }, + { + "name": "close_compressed_account_v2", + "discriminator": [ + 12, + 21, + 104, + 30, + 185, + 99, + 10, + 30 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "proof", + "type": { + "defined": { + "name": "ValidityProof" + } + } + }, + { + "name": "my_compressed_account", + "type": { + "defined": { + "name": "MyCompressedAccount" + } + } + }, + { + "name": "account_meta", + "type": { + "defined": { + "name": "CompressedAccountMeta" + } + } + } + ] + }, + { + "name": "create_compressed_account", + "discriminator": [ + 74, + 87, + 131, + 150, + 204, + 209, + 66, + 94 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "proof", + "type": { + "defined": { + "name": "ValidityProof" + } + } + }, + { + "name": "address_tree_info", + "type": { + "defined": { + "name": "PackedAddressTreeInfo" + } + } + }, + { + "name": "output_tree_index", + "type": "u8" + }, + { + "name": "name", + "type": "string" + } + ] + }, + { + "name": "create_compressed_account_v2", + "discriminator": [ + 16, + 69, + 137, + 87, + 207, + 37, + 81, + 138 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "proof", + "type": { + "defined": { + "name": "ValidityProof" + } + } + }, + { + "name": "address_tree_info", + "type": { + "defined": { + "name": "PackedAddressTreeInfo" + } + } + }, + { + "name": "output_tree_index", + "type": "u8" + }, + { + "name": "name", + "type": "string" + } + ] + }, + { + "name": "reinit_closed_account", + "discriminator": [ + 100, + 26, + 249, + 27, + 243, + 0, + 206, + 64 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "proof", + "type": { + "defined": { + "name": "ValidityProof" + } + } + }, + { + "name": "account_meta", + "type": { + "defined": { + "name": "CompressedAccountMeta" + } + } + } + ] + }, + { + "name": "update_compressed_account", + "discriminator": [ + 3, + 98, + 6, + 60, + 116, + 45, + 88, + 166 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "proof", + "type": { + "defined": { + "name": "ValidityProof" + } + } + }, + { + "name": "my_compressed_account", + "type": { + "defined": { + "name": "MyCompressedAccount" + } + } + }, + { + "name": "account_meta", + "type": { + "defined": { + "name": "CompressedAccountMeta" + } + } + }, + { + "name": "nested_data", + "type": { + "defined": { + "name": "NestedData" + } + } + } + ] + }, + { + "name": "update_compressed_account_v2", + "discriminator": [ + 100, + 134, + 47, + 184, + 220, + 7, + 96, + 236 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "proof", + "type": { + "defined": { + "name": "ValidityProof" + } + } + }, + { + "name": "my_compressed_account", + "type": { + "defined": { + "name": "MyCompressedAccount" + } + } + }, + { + "name": "account_meta", + "type": { + "defined": { + "name": "CompressedAccountMeta" + } + } + }, + { + "name": "nested_data", + "type": { + "defined": { + "name": "NestedData" + } + } + } + ] + }, + { + "name": "without_compressed_account", + "discriminator": [ + 68, + 84, + 81, + 196, + 24, + 131, + 208, + 209 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + }, + { + "name": "my_regular_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 109, + 112, + 114, + 101, + 115, + 115, + 101, + 100 + ] + }, + { + "kind": "arg", + "path": "name" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "name", + "type": "string" + } + ] + } + ], + "accounts": [ + { + "name": "MyRegularAccount", + "discriminator": [ + 186, + 181, + 76, + 117, + 61, + 130, + 63, + 14 + ] + } + ], + "events": [ + { + "name": "MyCompressedAccount", + "discriminator": [ + 147, + 40, + 99, + 80, + 53, + 44, + 10, + 210 + ] + } + ], + "types": [ + { + "name": "CompressedAccountMeta", + "type": { + "kind": "struct", + "fields": [ + { + "name": "tree_info", + "docs": [ + "Merkle tree context." + ], + "type": { + "defined": { + "name": "PackedStateTreeInfo" + } + } + }, + { + "name": "address", + "docs": [ + "Address." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "output_state_tree_index", + "docs": [ + "Output merkle tree index." + ], + "type": "u8" + } + ] + } + }, + { + "name": "CompressedAccountMetaBurn", + "type": { + "kind": "struct", + "fields": [ + { + "name": "tree_info", + "docs": [ + "State Merkle tree context." + ], + "type": { + "defined": { + "name": "PackedStateTreeInfo" + } + } + }, + { + "name": "address", + "docs": [ + "Address." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "CompressedProof", + "repr": { + "kind": "c" + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "a", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "b", + "type": { + "array": [ + "u8", + 64 + ] + } + }, + { + "name": "c", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "MyCompressedAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "nested", + "type": { + "defined": { + "name": "NestedData" + } + } + } + ] + } + }, + { + "name": "MyRegularAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + } + ] + } + }, + { + "name": "NestedData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "one", + "type": "u16" + }, + { + "name": "two", + "type": "u16" + }, + { + "name": "three", + "type": "u16" + }, + { + "name": "four", + "type": "u16" + }, + { + "name": "five", + "type": "u16" + }, + { + "name": "six", + "type": "u16" + }, + { + "name": "seven", + "type": "u16" + }, + { + "name": "eight", + "type": "u16" + }, + { + "name": "nine", + "type": "u16" + }, + { + "name": "ten", + "type": "u16" + }, + { + "name": "eleven", + "type": "u16" + }, + { + "name": "twelve", + "type": "u16" + } + ] + } + }, + { + "name": "PackedAddressTreeInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "address_merkle_tree_pubkey_index", + "type": "u8" + }, + { + "name": "address_queue_pubkey_index", + "type": "u8" + }, + { + "name": "root_index", + "type": "u16" + } + ] + } + }, + { + "name": "PackedStateTreeInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "root_index", + "type": "u16" + }, + { + "name": "prove_by_index", + "type": "bool" + }, + { + "name": "merkle_tree_pubkey_index", + "type": "u8" + }, + { + "name": "queue_pubkey_index", + "type": "u8" + }, + { + "name": "leaf_index", + "type": "u32" + } + ] + } + }, + { + "name": "ValidityProof", + "type": { + "kind": "struct", + "fields": [ + { + "option": { + "defined": { + "name": "CompressedProof" + } + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/sdk-tests/sdk-anchor-test/target/types/sdk_anchor_test.ts b/sdk-tests/sdk-anchor-test/target/types/sdk_anchor_test.ts new file mode 100644 index 0000000000..e805730cc1 --- /dev/null +++ b/sdk-tests/sdk-anchor-test/target/types/sdk_anchor_test.ts @@ -0,0 +1,732 @@ +/** + * Program IDL in camelCase format in order to be used in JS/TS. + * + * Note that this is only a type helper and is not the actual IDL. The original + * IDL can be found at `target/idl/sdk_anchor_test.json`. + */ +export type SdkAnchorTest = { + "address": "2tzfijPBGbrR5PboyFUFKzfEoLTwdDSHUjANCw929wyt", + "metadata": { + "name": "sdkAnchorTest", + "version": "0.7.0", + "spec": "0.1.0", + "description": "Test program for Light SDK and Light Macros" + }, + "instructions": [ + { + "name": "closeCompressedAccount", + "discriminator": [ + 55, + 108, + 99, + 108, + 119, + 228, + 247, + 203 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "proof", + "type": { + "defined": { + "name": "validityProof" + } + } + }, + { + "name": "myCompressedAccount", + "type": { + "defined": { + "name": "myCompressedAccount" + } + } + }, + { + "name": "accountMeta", + "type": { + "defined": { + "name": "compressedAccountMeta" + } + } + } + ] + }, + { + "name": "closeCompressedAccountPermanent", + "discriminator": [ + 117, + 145, + 242, + 98, + 46, + 187, + 118, + 125 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "proof", + "type": { + "defined": { + "name": "validityProof" + } + } + }, + { + "name": "accountMeta", + "type": { + "defined": { + "name": "compressedAccountMetaBurn" + } + } + } + ] + }, + { + "name": "closeCompressedAccountV2", + "discriminator": [ + 12, + 21, + 104, + 30, + 185, + 99, + 10, + 30 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "proof", + "type": { + "defined": { + "name": "validityProof" + } + } + }, + { + "name": "myCompressedAccount", + "type": { + "defined": { + "name": "myCompressedAccount" + } + } + }, + { + "name": "accountMeta", + "type": { + "defined": { + "name": "compressedAccountMeta" + } + } + } + ] + }, + { + "name": "createCompressedAccount", + "discriminator": [ + 74, + 87, + 131, + 150, + 204, + 209, + 66, + 94 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "proof", + "type": { + "defined": { + "name": "validityProof" + } + } + }, + { + "name": "addressTreeInfo", + "type": { + "defined": { + "name": "packedAddressTreeInfo" + } + } + }, + { + "name": "outputTreeIndex", + "type": "u8" + }, + { + "name": "name", + "type": "string" + } + ] + }, + { + "name": "createCompressedAccountV2", + "discriminator": [ + 16, + 69, + 137, + 87, + 207, + 37, + 81, + 138 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "proof", + "type": { + "defined": { + "name": "validityProof" + } + } + }, + { + "name": "addressTreeInfo", + "type": { + "defined": { + "name": "packedAddressTreeInfo" + } + } + }, + { + "name": "outputTreeIndex", + "type": "u8" + }, + { + "name": "name", + "type": "string" + } + ] + }, + { + "name": "reinitClosedAccount", + "discriminator": [ + 100, + 26, + 249, + 27, + 243, + 0, + 206, + 64 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "proof", + "type": { + "defined": { + "name": "validityProof" + } + } + }, + { + "name": "accountMeta", + "type": { + "defined": { + "name": "compressedAccountMeta" + } + } + } + ] + }, + { + "name": "updateCompressedAccount", + "discriminator": [ + 3, + 98, + 6, + 60, + 116, + 45, + 88, + 166 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "proof", + "type": { + "defined": { + "name": "validityProof" + } + } + }, + { + "name": "myCompressedAccount", + "type": { + "defined": { + "name": "myCompressedAccount" + } + } + }, + { + "name": "accountMeta", + "type": { + "defined": { + "name": "compressedAccountMeta" + } + } + }, + { + "name": "nestedData", + "type": { + "defined": { + "name": "nestedData" + } + } + } + ] + }, + { + "name": "updateCompressedAccountV2", + "discriminator": [ + 100, + 134, + 47, + 184, + 220, + 7, + 96, + 236 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "proof", + "type": { + "defined": { + "name": "validityProof" + } + } + }, + { + "name": "myCompressedAccount", + "type": { + "defined": { + "name": "myCompressedAccount" + } + } + }, + { + "name": "accountMeta", + "type": { + "defined": { + "name": "compressedAccountMeta" + } + } + }, + { + "name": "nestedData", + "type": { + "defined": { + "name": "nestedData" + } + } + } + ] + }, + { + "name": "withoutCompressedAccount", + "discriminator": [ + 68, + 84, + 81, + 196, + 24, + 131, + 208, + 209 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + }, + { + "name": "myRegularAccount", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 109, + 112, + 114, + 101, + 115, + 115, + 101, + 100 + ] + }, + { + "kind": "arg", + "path": "name" + } + ] + } + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "name", + "type": "string" + } + ] + } + ], + "accounts": [ + { + "name": "myRegularAccount", + "discriminator": [ + 186, + 181, + 76, + 117, + 61, + 130, + 63, + 14 + ] + } + ], + "events": [ + { + "name": "myCompressedAccount", + "discriminator": [ + 147, + 40, + 99, + 80, + 53, + 44, + 10, + 210 + ] + } + ], + "types": [ + { + "name": "compressedAccountMeta", + "type": { + "kind": "struct", + "fields": [ + { + "name": "treeInfo", + "docs": [ + "Merkle tree context." + ], + "type": { + "defined": { + "name": "packedStateTreeInfo" + } + } + }, + { + "name": "address", + "docs": [ + "Address." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "outputStateTreeIndex", + "docs": [ + "Output merkle tree index." + ], + "type": "u8" + } + ] + } + }, + { + "name": "compressedAccountMetaBurn", + "type": { + "kind": "struct", + "fields": [ + { + "name": "treeInfo", + "docs": [ + "State Merkle tree context." + ], + "type": { + "defined": { + "name": "packedStateTreeInfo" + } + } + }, + { + "name": "address", + "docs": [ + "Address." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "compressedProof", + "repr": { + "kind": "c" + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "a", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "b", + "type": { + "array": [ + "u8", + 64 + ] + } + }, + { + "name": "c", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "myCompressedAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "nested", + "type": { + "defined": { + "name": "nestedData" + } + } + } + ] + } + }, + { + "name": "myRegularAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + } + ] + } + }, + { + "name": "nestedData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "one", + "type": "u16" + }, + { + "name": "two", + "type": "u16" + }, + { + "name": "three", + "type": "u16" + }, + { + "name": "four", + "type": "u16" + }, + { + "name": "five", + "type": "u16" + }, + { + "name": "six", + "type": "u16" + }, + { + "name": "seven", + "type": "u16" + }, + { + "name": "eight", + "type": "u16" + }, + { + "name": "nine", + "type": "u16" + }, + { + "name": "ten", + "type": "u16" + }, + { + "name": "eleven", + "type": "u16" + }, + { + "name": "twelve", + "type": "u16" + } + ] + } + }, + { + "name": "packedAddressTreeInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "addressMerkleTreePubkeyIndex", + "type": "u8" + }, + { + "name": "addressQueuePubkeyIndex", + "type": "u8" + }, + { + "name": "rootIndex", + "type": "u16" + } + ] + } + }, + { + "name": "packedStateTreeInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "rootIndex", + "type": "u16" + }, + { + "name": "proveByIndex", + "type": "bool" + }, + { + "name": "merkleTreePubkeyIndex", + "type": "u8" + }, + { + "name": "queuePubkeyIndex", + "type": "u8" + }, + { + "name": "leafIndex", + "type": "u32" + } + ] + } + }, + { + "name": "validityProof", + "type": { + "kind": "struct", + "fields": [ + { + "option": { + "defined": { + "name": "compressedProof" + } + } + } + ] + } + } + ] +}; From c89bf730dfa563e7f60c4578a8a8f6eee23f63b8 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Thu, 19 Feb 2026 18:06:57 +0000 Subject: [PATCH 09/26] fix --- .../tests/light_token/approve_revoke.rs | 1 + .../tests/light_token/shared.rs | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs b/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs index b2bea672cf..82415b4ec5 100644 --- a/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs +++ b/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs @@ -80,6 +80,7 @@ async fn test_approve_fails() { delegate.pubkey(), &owner, 100, + None, "non_existent_account", 6153, // NotRentExempt (SPL Token code 0 -> ErrorCode::NotRentExempt) ) diff --git a/program-tests/compressed-token-test/tests/light_token/shared.rs b/program-tests/compressed-token-test/tests/light_token/shared.rs index bcd00879bb..f1d65ad313 100644 --- a/program-tests/compressed-token-test/tests/light_token/shared.rs +++ b/program-tests/compressed-token-test/tests/light_token/shared.rs @@ -935,12 +935,14 @@ pub async fn approve_and_assert_fails( delegate: Pubkey, authority: &Keypair, amount: u64, + max_top_up: Option, name: &str, expected_error_code: u32, ) { println!("Approve (expecting failure) initiated for: {}", name); - let instruction = Approve { + // Build using SDK, then modify if needed for max_top_up + let mut instruction = Approve { token_account, delegate, owner: authority.pubkey(), @@ -949,6 +951,11 @@ pub async fn approve_and_assert_fails( .instruction() .unwrap(); + // Add max_top_up to instruction data if specified + if let Some(max) = max_top_up { + instruction.data.extend_from_slice(&max.to_le_bytes()); + } + let result = context .rpc .create_and_send_transaction( @@ -992,18 +999,25 @@ pub async fn revoke_and_assert_fails( context: &mut AccountTestContext, token_account: Pubkey, authority: &Keypair, + max_top_up: Option, name: &str, expected_error_code: u32, ) { println!("Revoke (expecting failure) initiated for: {}", name); - let instruction = Revoke { + // Build using SDK, then modify if needed for max_top_up + let mut instruction = Revoke { token_account, owner: authority.pubkey(), } .instruction() .unwrap(); + // Add max_top_up to instruction data if specified + if let Some(max) = max_top_up { + instruction.data.extend_from_slice(&max.to_le_bytes()); + } + let result = context .rpc .create_and_send_transaction( From 28c394d73c2e1b8505c7649bf3060f75fcf8972d Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Thu, 19 Feb 2026 18:09:49 +0000 Subject: [PATCH 10/26] fix(tests): restore max_top_up exceeded tests for approve and revoke MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore the raw builder test pattern that verifies the on-chain MaxTopUpExceeded error path. These tests use the SDK instruction builder, then append max_top_up bytes directly to test the on-chain limit — same pattern used for Transfer. --- .../tests/light_token/approve_revoke.rs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs b/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs index 82415b4ec5..cd1bb64d41 100644 --- a/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs +++ b/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs @@ -10,6 +10,7 @@ //! | Invalid ctoken (non-existent) | test_approve_fails | test_revoke_fails | //! | Invalid ctoken (wrong owner) | test_approve_fails | test_revoke_fails | //! | Invalid ctoken (spl account) | test_approve_fails | test_revoke_fails | +//! | Max top-up exceeded | test_approve_fails | test_revoke_fails | //! //! **Note**: "Invalid mint" tests not applicable - approve/revoke don't take mint as account. @@ -128,11 +129,44 @@ async fn test_approve_fails() { delegate.pubkey(), &owner, 100, + None, "wrong_program_owner", 13, // InstructionError::ExternalAccountDataModified - program tried to modify account it doesn't own ) .await; } + + // Test 3: Max top-up exceeded + { + let mut context = setup_account_test_with_created_account(Some((10, false))) + .await + .unwrap(); + + // Fund owner so the test doesn't fail due to insufficient lamports + context + .rpc + .airdrop_lamports(&context.owner_keypair.pubkey(), 1_000_000_000) + .await + .unwrap(); + + // Warp time to trigger top-up requirement (past funded epochs) + context.rpc.warp_to_slot(SLOTS_PER_EPOCH * 12 + 1).unwrap(); + + let delegate = Keypair::new(); + let token_account = context.token_account_keypair.pubkey(); + let owner = context.owner_keypair.insecure_clone(); + approve_and_assert_fails( + &mut context, + token_account, + delegate.pubkey(), + &owner, + 100, + Some(1), // max_top_up = 1 (1,000 lamports budget, still too low for rent top-up) + "max_topup_exceeded", + 18043, // TokenError::MaxTopUpExceeded + ) + .await; + } } // ============================================================================ @@ -217,6 +251,7 @@ async fn test_revoke_fails() { &mut context, non_existent, &owner, + None, "non_existent_account", 6153, // NotRentExempt (SPL Token code 0 -> ErrorCode::NotRentExempt) ) @@ -261,11 +296,43 @@ async fn test_revoke_fails() { &mut context, wrong_owner_account.pubkey(), &owner, + None, "wrong_program_owner", 13, // InstructionError::ExternalAccountDataModified - program tried to modify account it doesn't own ) .await; } + + // Test 3: Max top-up exceeded + { + let mut context = setup_account_test_with_created_account(Some((10, false))) + .await + .unwrap(); + + // First approve to set delegate (need to do before warping) + context + .rpc + .airdrop_lamports(&context.owner_keypair.pubkey(), 1_000_000_000) + .await + .unwrap(); + let delegate = Keypair::new(); + approve_and_assert(&mut context, delegate.pubkey(), 100, "approve_before_warp").await; + + // Warp time to trigger top-up requirement (past funded epochs) + context.rpc.warp_to_slot(SLOTS_PER_EPOCH * 12 + 1).unwrap(); + + let token_account = context.token_account_keypair.pubkey(); + let owner = context.owner_keypair.insecure_clone(); + revoke_and_assert_fails( + &mut context, + token_account, + &owner, + Some(1), // max_top_up = 1 (1,000 lamports budget, still too low for rent top-up) + "max_topup_exceeded", + 18043, // TokenError::MaxTopUpExceeded + ) + .await; + } } // ============================================================================ From 5023ccf7ab1b9c64048d43b17cb5827a36500f24 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Thu, 19 Feb 2026 18:39:12 +0000 Subject: [PATCH 11/26] fix(token-sdk): use short wire format to match pinocchio (no max_top_up bytes) --- sdk-libs/token-sdk/src/instruction/burn.rs | 1 - sdk-libs/token-sdk/src/instruction/burn_checked.rs | 1 - sdk-libs/token-sdk/src/instruction/mint_to.rs | 1 - sdk-libs/token-sdk/src/instruction/mint_to_checked.rs | 1 - sdk-libs/token-sdk/src/instruction/transfer.rs | 1 - sdk-libs/token-sdk/src/instruction/transfer_checked.rs | 1 - sdk-libs/token-sdk/tests/instruction_transfer.rs | 10 ++++------ 7 files changed, 4 insertions(+), 12 deletions(-) diff --git a/sdk-libs/token-sdk/src/instruction/burn.rs b/sdk-libs/token-sdk/src/instruction/burn.rs index 0e9d7a3980..d608c78910 100644 --- a/sdk-libs/token-sdk/src/instruction/burn.rs +++ b/sdk-libs/token-sdk/src/instruction/burn.rs @@ -143,7 +143,6 @@ impl Burn { data: { let mut data = vec![8u8]; // CTokenBurn discriminator data.extend_from_slice(&self.amount.to_le_bytes()); - data.extend_from_slice(&u16::MAX.to_le_bytes()); data }, }) diff --git a/sdk-libs/token-sdk/src/instruction/burn_checked.rs b/sdk-libs/token-sdk/src/instruction/burn_checked.rs index 950850714f..393499a13b 100644 --- a/sdk-libs/token-sdk/src/instruction/burn_checked.rs +++ b/sdk-libs/token-sdk/src/instruction/burn_checked.rs @@ -150,7 +150,6 @@ impl BurnChecked { let mut data = vec![15u8]; // CTokenBurnChecked discriminator data.extend_from_slice(&self.amount.to_le_bytes()); data.push(self.decimals); - data.extend_from_slice(&u16::MAX.to_le_bytes()); data }, }) diff --git a/sdk-libs/token-sdk/src/instruction/mint_to.rs b/sdk-libs/token-sdk/src/instruction/mint_to.rs index 2976b490ef..ed03074087 100644 --- a/sdk-libs/token-sdk/src/instruction/mint_to.rs +++ b/sdk-libs/token-sdk/src/instruction/mint_to.rs @@ -153,7 +153,6 @@ impl MintTo { data: { let mut data = vec![7u8]; // MintTo discriminator data.extend_from_slice(&self.amount.to_le_bytes()); - data.extend_from_slice(&u16::MAX.to_le_bytes()); data }, }) diff --git a/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs b/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs index bad8487112..e915d496bb 100644 --- a/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs +++ b/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs @@ -160,7 +160,6 @@ impl MintToChecked { let mut data = vec![14u8]; // TokenMintToChecked discriminator data.extend_from_slice(&self.amount.to_le_bytes()); data.push(self.decimals); - data.extend_from_slice(&u16::MAX.to_le_bytes()); data }, }) diff --git a/sdk-libs/token-sdk/src/instruction/transfer.rs b/sdk-libs/token-sdk/src/instruction/transfer.rs index beeb3e4c0d..f4390f52ed 100644 --- a/sdk-libs/token-sdk/src/instruction/transfer.rs +++ b/sdk-libs/token-sdk/src/instruction/transfer.rs @@ -150,7 +150,6 @@ impl Transfer { data: { let mut data = vec![3u8]; data.extend_from_slice(&self.amount.to_le_bytes()); - data.extend_from_slice(&u16::MAX.to_le_bytes()); data }, }) diff --git a/sdk-libs/token-sdk/src/instruction/transfer_checked.rs b/sdk-libs/token-sdk/src/instruction/transfer_checked.rs index acdf0ad1a9..d21daffbf0 100644 --- a/sdk-libs/token-sdk/src/instruction/transfer_checked.rs +++ b/sdk-libs/token-sdk/src/instruction/transfer_checked.rs @@ -167,7 +167,6 @@ impl TransferChecked { let mut data = vec![12u8]; // TransferChecked discriminator (SPL compatible) data.extend_from_slice(&self.amount.to_le_bytes()); data.push(self.decimals); - data.extend_from_slice(&u16::MAX.to_le_bytes()); data }, }) diff --git a/sdk-libs/token-sdk/tests/instruction_transfer.rs b/sdk-libs/token-sdk/tests/instruction_transfer.rs index ff2859068a..df829bf976 100644 --- a/sdk-libs/token-sdk/tests/instruction_transfer.rs +++ b/sdk-libs/token-sdk/tests/instruction_transfer.rs @@ -4,7 +4,7 @@ use solana_pubkey::Pubkey; /// Test Transfer instruction without fee_payer. /// Authority is writable signer since it pays for top-ups. -/// Data always includes max_top_up as u16::MAX. +/// Short format: discriminator (1) + amount (8) = 9 bytes. #[test] fn test_transfer_basic() { let source = Pubkey::new_from_array([1u8; 32]); @@ -23,7 +23,7 @@ fn test_transfer_basic() { // Hardcoded expected instruction // - authority is writable (no fee_payer -> authority pays for top-ups) - // - data: discriminator (3) + amount (8 bytes) + max_top_up u16::MAX (2 bytes) = 11 bytes + // - data: discriminator (3) + amount (8 bytes) = 9 bytes (short format, on-chain defaults max_top_up to u16::MAX) let expected = Instruction { program_id: LIGHT_TOKEN_PROGRAM_ID, accounts: vec![ @@ -35,7 +35,6 @@ fn test_transfer_basic() { data: vec![ 3u8, // Transfer discriminator 100, 0, 0, 0, 0, 0, 0, 0, // amount: 100 as little-endian u64 - 255, 255, // max_top_up: u16::MAX as little-endian u16 ], }; @@ -47,7 +46,7 @@ fn test_transfer_basic() { /// Test Transfer instruction with fee_payer set. /// Fee_payer is added as 5th account. Authority is readonly. -/// Data always includes max_top_up as u16::MAX. +/// Short format: discriminator (1) + amount (8) = 9 bytes. #[test] fn test_transfer_with_fee_payer() { let source = Pubkey::new_from_array([1u8; 32]); @@ -68,7 +67,7 @@ fn test_transfer_with_fee_payer() { // Hardcoded expected instruction // - authority is readonly (fee_payer pays instead) // - fee_payer is 5th account: writable, signer - // - data: discriminator (3) + amount (8 bytes) + max_top_up u16::MAX (2 bytes) = 11 bytes + // - data: discriminator (3) + amount (8 bytes) = 9 bytes (short format) let expected = Instruction { program_id: LIGHT_TOKEN_PROGRAM_ID, accounts: vec![ @@ -81,7 +80,6 @@ fn test_transfer_with_fee_payer() { data: vec![ 3u8, // Transfer discriminator 100, 0, 0, 0, 0, 0, 0, 0, // amount: 100 as little-endian u64 - 255, 255, // max_top_up: u16::MAX as little-endian u16 ], }; From 2c64355d971223df23bf5d8a4addca80cdf49833 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Thu, 19 Feb 2026 18:42:11 +0000 Subject: [PATCH 12/26] style: fix formatting in test files --- sdk-tests/sdk-light-token-test/tests/test_burn.rs | 4 ++-- .../tests/test_ctoken_mint_to.rs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sdk-tests/sdk-light-token-test/tests/test_burn.rs b/sdk-tests/sdk-light-token-test/tests/test_burn.rs index e09d2b3f63..99428eb778 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_burn.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_burn.rs @@ -119,9 +119,9 @@ async fn test_burn_invoke_signed() { accounts: vec![ AccountMeta::new(ata, false), // source AccountMeta::new(mint_pda, false), // mint - AccountMeta::new(pda_owner, false), // PDA authority (writable, pays for top-ups) + AccountMeta::new(pda_owner, false), // PDA authority (writable, pays for top-ups) AccountMeta::new_readonly(light_token_program, false), // light_token_program - AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(Pubkey::default(), false), // system_program ], data: instruction_data, }; diff --git a/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs b/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs index f61da04471..96e2de66b8 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs @@ -169,18 +169,18 @@ async fn test_ctoken_mint_to_invoke_signed() { AccountMeta::new_readonly(compressed_token_program_id, false), // [0] AccountMeta::new_readonly(default_pubkeys.light_system_program, false), // [1] AccountMeta::new_readonly(mint_signer_pda, false), // [2] mint_signer PDA - AccountMeta::new(pda_mint_authority, false), // [3] authority PDA (writable, pays for top-ups) + AccountMeta::new(pda_mint_authority, false), // [3] authority PDA (writable, pays for top-ups) AccountMeta::new_readonly(compressible_config, false), // [4] compressible_config - AccountMeta::new(mint_pda, false), // [5] mint - AccountMeta::new(rent_sponsor, false), // [6] rent_sponsor - AccountMeta::new(payer.pubkey(), true), // [7] fee_payer (signer) + AccountMeta::new(mint_pda, false), // [5] mint + AccountMeta::new(rent_sponsor, false), // [6] rent_sponsor + AccountMeta::new(payer.pubkey(), true), // [7] fee_payer (signer) AccountMeta::new_readonly(default_pubkeys.cpi_authority_pda, false), // [8] AccountMeta::new_readonly(default_pubkeys.registered_program_pda, false), // [9] AccountMeta::new_readonly(default_pubkeys.account_compression_authority, false), // [10] AccountMeta::new_readonly(default_pubkeys.account_compression_program, false), // [11] AccountMeta::new_readonly(default_pubkeys.system_program, false), // [12] - AccountMeta::new(output_queue, false), // [13] - AccountMeta::new(address_tree.tree, false), // [14] + AccountMeta::new(output_queue, false), // [13] + AccountMeta::new(address_tree.tree, false), // [14] ]; let create_mint_ix = Instruction { From 0762ce068b501160a4fb850024ef89550b6084c0 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Fri, 20 Feb 2026 15:27:39 +0000 Subject: [PATCH 13/26] fix(token-sdk): mandatory fee_payer, authority always readonly, builder max_top_up Redesign all token instruction structs so that fee_payer is a mandatory field and authority/owner is always readonly (fee_payer pays for top-ups instead). Remove max_top_up from struct fields and add .with_max_top_up() builder pattern that appends 2 bytes to the wire format. Exception: Approve/Revoke keep owner writable (on-chain doesn't support fee_payer yet), but the fee_payer field exists for API consistency. Update all CPI structs (solana AccountInfo + pinocchio) accordingly, and fix invoke_signed test programs to pass a separate fee_payer account since PDA authority != transaction fee payer. --- .../tests/light_token/approve_revoke.rs | 2 + .../tests/light_token/burn.rs | 20 +-- .../tests/light_token/extensions_failing.rs | 6 +- .../tests/light_token/shared.rs | 4 + .../tests/light_token/transfer.rs | 40 ++---- .../tests/light_token/transfer_checked.rs | 4 +- .../compressed-token-test/tests/mint/burn.rs | 4 +- .../tests/mint/mint_to.rs | 8 +- .../registry-test/tests/compressible.rs | 2 +- sdk-libs/token-client/src/actions/approve.rs | 2 + sdk-libs/token-client/src/actions/mint_to.rs | 9 +- sdk-libs/token-client/src/actions/revoke.rs | 2 + sdk-libs/token-client/src/actions/transfer.rs | 9 +- .../src/actions/transfer_checked.rs | 9 +- .../src/instruction/approve.rs | 5 + .../token-pinocchio/src/instruction/burn.rs | 90 ++++--------- .../src/instruction/burn_checked.rs | 90 ++++--------- .../src/instruction/mint_to.rs | 95 ++++---------- .../src/instruction/mint_to_checked.rs | 95 ++++---------- .../token-pinocchio/src/instruction/revoke.rs | 5 + .../src/instruction/transfer.rs | 95 ++++---------- .../src/instruction/transfer_interface.rs | 4 +- sdk-libs/token-pinocchio/src/lib.rs | 2 +- sdk-libs/token-sdk/src/instruction/approve.rs | 15 ++- sdk-libs/token-sdk/src/instruction/burn.rs | 104 ++++++++------- .../token-sdk/src/instruction/burn_checked.rs | 106 +++++++-------- sdk-libs/token-sdk/src/instruction/mint_to.rs | 114 ++++++++-------- .../src/instruction/mint_to_checked.rs | 116 ++++++++--------- sdk-libs/token-sdk/src/instruction/revoke.rs | 15 ++- .../token-sdk/src/instruction/transfer.rs | 115 ++++++++--------- .../src/instruction/transfer_checked.rs | 122 ++++++++---------- .../src/instruction/transfer_interface.rs | 2 +- .../token-sdk/tests/instruction_transfer.rs | 47 +++---- sdk-libs/token-sdk/tests/transfer_type.rs | 6 + .../src/amm_test/deposit.rs | 2 +- .../src/amm_test/initialize.rs | 2 +- .../src/amm_test/withdraw.rs | 2 +- .../csdk-anchor-full-derived-test/src/lib.rs | 6 +- .../tests/shared.rs | 2 +- .../sdk-light-token-pinocchio/src/approve.rs | 2 + .../sdk-light-token-pinocchio/src/burn.rs | 7 +- .../src/ctoken_mint_to.rs | 7 +- .../sdk-light-token-pinocchio/src/revoke.rs | 2 + .../sdk-light-token-pinocchio/src/transfer.rs | 7 +- .../src/transfer_checked.rs | 7 +- .../tests/shared/mod.rs | 6 +- .../tests/test_burn.rs | 1 + .../tests/test_ctoken_mint_to.rs | 3 +- .../tests/test_transfer.rs | 1 + sdk-tests/sdk-light-token-test/src/approve.rs | 2 + sdk-tests/sdk-light-token-test/src/burn.rs | 7 +- .../src/ctoken_mint_to.rs | 7 +- sdk-tests/sdk-light-token-test/src/revoke.rs | 2 + .../sdk-light-token-test/src/transfer.rs | 7 +- .../src/transfer_checked.rs | 7 +- .../tests/scenario_light_mint.rs | 2 +- .../scenario_light_mint_compression_only.rs | 2 +- .../sdk-light-token-test/tests/shared.rs | 6 +- .../sdk-light-token-test/tests/test_burn.rs | 5 +- .../tests/test_ctoken_mint_to.rs | 17 +-- .../tests/test_transfer.rs | 3 +- 61 files changed, 655 insertions(+), 833 deletions(-) diff --git a/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs b/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs index cd1bb64d41..ad296ea216 100644 --- a/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs +++ b/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs @@ -436,6 +436,7 @@ async fn test_approve_revoke_compressible() -> Result<(), RpcError> { delegate: delegate.pubkey(), owner: owner.pubkey(), amount: approve_amount, + fee_payer: payer.pubkey(), } .instruction() .map_err(|e| { @@ -460,6 +461,7 @@ async fn test_approve_revoke_compressible() -> Result<(), RpcError> { let revoke_ix = Revoke { token_account: account_pubkey, owner: owner.pubkey(), + fee_payer: payer.pubkey(), } .instruction() .map_err(|e| RpcError::AssertRpcError(format!("Failed to create revoke instruction: {}", e)))?; diff --git a/program-tests/compressed-token-test/tests/light_token/burn.rs b/program-tests/compressed-token-test/tests/light_token/burn.rs index 23db86b1a4..a0fb87ed66 100644 --- a/program-tests/compressed-token-test/tests/light_token/burn.rs +++ b/program-tests/compressed-token-test/tests/light_token/burn.rs @@ -47,7 +47,7 @@ async fn test_burn_success_cases() { mint: ctx.mint_pda, amount: burn_amount, authority: ctx.owner_keypair.pubkey(), - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -78,7 +78,7 @@ async fn test_burn_success_cases() { mint: ctx.mint_pda, amount: burn_amount, authority: ctx.owner_keypair.pubkey(), - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -128,7 +128,7 @@ async fn test_burn_fails() { mint: other_mint_pda, // Wrong mint amount: 50, authority: ctx.owner_keypair.pubkey(), - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -158,7 +158,7 @@ async fn test_burn_fails() { mint: ctx.mint_pda, amount: 50, authority: ctx.owner_keypair.pubkey(), - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -204,7 +204,7 @@ async fn test_burn_fails() { mint: ctx.mint_pda, amount: 50, authority: ctx.owner_keypair.pubkey(), - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -234,7 +234,7 @@ async fn test_burn_fails() { mint: ctx.mint_pda, amount: 200, // More than 100 balance authority: ctx.owner_keypair.pubkey(), - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -269,7 +269,7 @@ async fn test_burn_fails() { mint: ctx.mint_pda, amount: 50, authority: wrong_authority.pubkey(), - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -372,7 +372,7 @@ async fn setup_burn_test() -> BurnTestContext { destination: ctoken_ata, amount: 100, authority: mint_authority.pubkey(), - fee_payer: None, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); @@ -417,7 +417,7 @@ async fn test_burn_checked_success() { amount: burn_amount, decimals: 8, // Correct decimals authority: ctx.owner_keypair.pubkey(), - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -449,7 +449,7 @@ async fn test_burn_checked_wrong_decimals() { amount: 50, decimals: 7, // Wrong decimals authority: ctx.owner_keypair.pubkey(), - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); diff --git a/program-tests/compressed-token-test/tests/light_token/extensions_failing.rs b/program-tests/compressed-token-test/tests/light_token/extensions_failing.rs index ae48e7b852..58aecd643f 100644 --- a/program-tests/compressed-token-test/tests/light_token/extensions_failing.rs +++ b/program-tests/compressed-token-test/tests/light_token/extensions_failing.rs @@ -195,7 +195,7 @@ async fn test_ctoken_transfer_fails_when_mint_paused() { amount: 100_000_000, decimals: 9, authority: owner.pubkey(), - fee_payer: None, + fee_payer: context.payer.pubkey(), } .instruction() .unwrap(); @@ -242,7 +242,7 @@ async fn test_ctoken_transfer_fails_with_non_zero_transfer_fee() { amount: 100_000_000, decimals: 9, authority: owner.pubkey(), - fee_payer: None, + fee_payer: context.payer.pubkey(), } .instruction() .unwrap(); @@ -290,7 +290,7 @@ async fn test_ctoken_transfer_fails_with_non_nil_transfer_hook() { amount: 100_000_000, decimals: 9, authority: owner.pubkey(), - fee_payer: None, + fee_payer: context.payer.pubkey(), } .instruction() .unwrap(); diff --git a/program-tests/compressed-token-test/tests/light_token/shared.rs b/program-tests/compressed-token-test/tests/light_token/shared.rs index f1d65ad313..b0a025ff58 100644 --- a/program-tests/compressed-token-test/tests/light_token/shared.rs +++ b/program-tests/compressed-token-test/tests/light_token/shared.rs @@ -903,6 +903,7 @@ pub async fn approve_and_assert( delegate, owner: context.owner_keypair.pubkey(), amount, + fee_payer: context.payer.pubkey(), } .instruction() .unwrap(); @@ -947,6 +948,7 @@ pub async fn approve_and_assert_fails( delegate, owner: authority.pubkey(), amount, + fee_payer: context.payer.pubkey(), } .instruction() .unwrap(); @@ -976,6 +978,7 @@ pub async fn revoke_and_assert(context: &mut AccountTestContext, name: &str) { let revoke_ix = Revoke { token_account: context.token_account_keypair.pubkey(), owner: context.owner_keypair.pubkey(), + fee_payer: context.payer.pubkey(), } .instruction() .unwrap(); @@ -1009,6 +1012,7 @@ pub async fn revoke_and_assert_fails( let mut instruction = Revoke { token_account, owner: authority.pubkey(), + fee_payer: context.payer.pubkey(), } .instruction() .unwrap(); diff --git a/program-tests/compressed-token-test/tests/light_token/transfer.rs b/program-tests/compressed-token-test/tests/light_token/transfer.rs index 0971f88cd5..be975a9148 100644 --- a/program-tests/compressed-token-test/tests/light_token/transfer.rs +++ b/program-tests/compressed-token-test/tests/light_token/transfer.rs @@ -860,7 +860,7 @@ async fn transfer_checked_and_assert( amount, decimals, authority: authority.pubkey(), - fee_payer: None, + fee_payer: context.payer.pubkey(), } .instruction() .unwrap(); @@ -901,7 +901,7 @@ async fn transfer_checked_and_assert_fails( amount, decimals, authority: authority.pubkey(), - fee_payer: None, + fee_payer: context.payer.pubkey(), } .instruction() .unwrap(); @@ -1066,30 +1066,18 @@ async fn test_ctoken_transfer_checked_max_top_up_exceeded() { let owner_keypair = context.owner_keypair.insecure_clone(); let payer_pubkey = context.payer.pubkey(); - // Build raw instruction with low max_top_up to test on-chain error path - // (TransferChecked struct no longer exposes max_top_up) - let transfer_ix = { - use anchor_lang::prelude::AccountMeta; - use solana_sdk::instruction::Instruction; - - // Discriminator (12) + amount (8) + decimals (1) + max_top_up (2) - let mut data = vec![12u8]; - data.extend_from_slice(&100u64.to_le_bytes()); - data.push(9u8); // decimals - data.extend_from_slice(&1u16.to_le_bytes()); // max_top_up = 1 - - Instruction { - program_id: light_compressed_token::ID, - accounts: vec![ - AccountMeta::new(source, false), - AccountMeta::new_readonly(mint, false), - AccountMeta::new(destination, false), - AccountMeta::new(owner_keypair.pubkey(), true), - AccountMeta::new_readonly(solana_sdk::system_program::ID, false), - ], - data, - } - }; + let transfer_ix = TransferChecked { + source, + mint, + destination, + amount: 100, + decimals: 9, + authority: owner_keypair.pubkey(), + fee_payer: context.payer.pubkey(), + } + .with_max_top_up(1) + .instruction() + .unwrap(); let result = context .rpc diff --git a/program-tests/compressed-token-test/tests/light_token/transfer_checked.rs b/program-tests/compressed-token-test/tests/light_token/transfer_checked.rs index f91a0f18a9..c3e4810f0e 100644 --- a/program-tests/compressed-token-test/tests/light_token/transfer_checked.rs +++ b/program-tests/compressed-token-test/tests/light_token/transfer_checked.rs @@ -162,7 +162,7 @@ async fn test_transfer_requires_checked_for_restricted_extensions() { destination: account_b_pubkey, amount: transfer_amount, authority: owner.pubkey(), - fee_payer: None, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); @@ -185,7 +185,7 @@ async fn test_transfer_requires_checked_for_restricted_extensions() { amount: transfer_amount, decimals: 9, authority: owner.pubkey(), - fee_payer: None, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); diff --git a/program-tests/compressed-token-test/tests/mint/burn.rs b/program-tests/compressed-token-test/tests/mint/burn.rs index 42c279df99..1811c60784 100644 --- a/program-tests/compressed-token-test/tests/mint/burn.rs +++ b/program-tests/compressed-token-test/tests/mint/burn.rs @@ -118,7 +118,7 @@ async fn test_ctoken_burn() { mint: ctx.mint_pda, amount: 500, authority: ctx.owner_keypair.pubkey(), - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -140,7 +140,7 @@ async fn test_ctoken_burn() { mint: ctx.mint_pda, amount: 500, authority: ctx.owner_keypair.pubkey(), - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); diff --git a/program-tests/compressed-token-test/tests/mint/mint_to.rs b/program-tests/compressed-token-test/tests/mint/mint_to.rs index fa469af6df..957565e2bd 100644 --- a/program-tests/compressed-token-test/tests/mint/mint_to.rs +++ b/program-tests/compressed-token-test/tests/mint/mint_to.rs @@ -96,7 +96,7 @@ async fn test_ctoken_mint_to() { destination: ctx.ctoken_account, amount: 500, authority: ctx.mint_authority.pubkey(), - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -118,7 +118,7 @@ async fn test_ctoken_mint_to() { destination: ctx.ctoken_account, amount: 500, authority: ctx.mint_authority.pubkey(), - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -169,7 +169,7 @@ async fn test_ctoken_mint_to_checked_success() { amount: 500, decimals: 8, // Correct decimals authority: ctx.mint_authority.pubkey(), - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -211,7 +211,7 @@ async fn test_ctoken_mint_to_checked_wrong_decimals() { amount: 500, decimals: 7, // Wrong decimals authority: ctx.mint_authority.pubkey(), - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); diff --git a/program-tests/registry-test/tests/compressible.rs b/program-tests/registry-test/tests/compressible.rs index 76daa90b74..9195fe76f0 100644 --- a/program-tests/registry-test/tests/compressible.rs +++ b/program-tests/registry-test/tests/compressible.rs @@ -1253,7 +1253,7 @@ async fn mint_to_token( destination, amount, authority: mint_authority.pubkey(), - fee_payer: None, + fee_payer: payer.pubkey(), } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create MintTo instruction: {:?}", e)))?; diff --git a/sdk-libs/token-client/src/actions/approve.rs b/sdk-libs/token-client/src/actions/approve.rs index 547acb1b1d..63fdb84aff 100644 --- a/sdk-libs/token-client/src/actions/approve.rs +++ b/sdk-libs/token-client/src/actions/approve.rs @@ -79,6 +79,7 @@ impl Approve { delegate: self.delegate, owner: owner_pubkey, amount: self.amount, + fee_payer: payer.pubkey(), } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; @@ -121,6 +122,7 @@ impl Approve { delegate: self.delegate, owner: owner.pubkey(), amount: self.amount, + fee_payer: payer.pubkey(), } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; diff --git a/sdk-libs/token-client/src/actions/mint_to.rs b/sdk-libs/token-client/src/actions/mint_to.rs index 38b3146831..0782b28e49 100644 --- a/sdk-libs/token-client/src/actions/mint_to.rs +++ b/sdk-libs/token-client/src/actions/mint_to.rs @@ -46,19 +46,12 @@ impl MintTo { payer: &Keypair, authority: &Keypair, ) -> Result { - // Only set fee_payer if payer differs from authority - let fee_payer = if payer.pubkey() != authority.pubkey() { - Some(payer.pubkey()) - } else { - None - }; - let ix = MintToInstruction { mint: self.mint, destination: self.destination, amount: self.amount, authority: authority.pubkey(), - fee_payer, + fee_payer: payer.pubkey(), } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; diff --git a/sdk-libs/token-client/src/actions/revoke.rs b/sdk-libs/token-client/src/actions/revoke.rs index 803b4516ad..3852d4841d 100644 --- a/sdk-libs/token-client/src/actions/revoke.rs +++ b/sdk-libs/token-client/src/actions/revoke.rs @@ -69,6 +69,7 @@ impl Revoke { let ix = RevokeInstruction { token_account: self.token_account, owner: owner_pubkey, + fee_payer: payer.pubkey(), } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; @@ -95,6 +96,7 @@ impl Revoke { let ix = RevokeInstruction { token_account: self.token_account, owner: owner.pubkey(), + fee_payer: payer.pubkey(), } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; diff --git a/sdk-libs/token-client/src/actions/transfer.rs b/sdk-libs/token-client/src/actions/transfer.rs index c41f867d58..9ab8ae9e43 100644 --- a/sdk-libs/token-client/src/actions/transfer.rs +++ b/sdk-libs/token-client/src/actions/transfer.rs @@ -46,19 +46,12 @@ impl Transfer { payer: &Keypair, authority: &Keypair, ) -> Result { - // Only set fee_payer if payer differs from authority - let fee_payer = if payer.pubkey() != authority.pubkey() { - Some(payer.pubkey()) - } else { - None - }; - let ix = TransferInstruction { source: self.source, destination: self.destination, amount: self.amount, authority: authority.pubkey(), - fee_payer, + fee_payer: payer.pubkey(), } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; diff --git a/sdk-libs/token-client/src/actions/transfer_checked.rs b/sdk-libs/token-client/src/actions/transfer_checked.rs index 8480336417..63bef2cbb7 100644 --- a/sdk-libs/token-client/src/actions/transfer_checked.rs +++ b/sdk-libs/token-client/src/actions/transfer_checked.rs @@ -55,13 +55,6 @@ impl TransferChecked { payer: &Keypair, authority: &Keypair, ) -> Result { - // Only set fee_payer if payer differs from authority - let fee_payer = if payer.pubkey() != authority.pubkey() { - Some(payer.pubkey()) - } else { - None - }; - let ix = TransferCheckedInstruction { source: self.source, mint: self.mint, @@ -69,7 +62,7 @@ impl TransferChecked { amount: self.amount, decimals: self.decimals, authority: authority.pubkey(), - fee_payer, + fee_payer: payer.pubkey(), } .instruction() .map_err(|e| RpcError::CustomError(format!("Failed to create instruction: {}", e)))?; diff --git a/sdk-libs/token-pinocchio/src/instruction/approve.rs b/sdk-libs/token-pinocchio/src/instruction/approve.rs index af6f821271..e50d7831ad 100644 --- a/sdk-libs/token-pinocchio/src/instruction/approve.rs +++ b/sdk-libs/token-pinocchio/src/instruction/approve.rs @@ -23,6 +23,7 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// owner: &ctx.accounts.owner, /// system_program: &ctx.accounts.system_program, /// amount: 100, +/// fee_payer: &ctx.accounts.fee_payer, /// } /// .invoke()?; /// ``` @@ -32,6 +33,9 @@ pub struct ApproveCpi<'info> { pub owner: &'info AccountInfo, pub system_program: &'info AccountInfo, pub amount: u64, + // TODO: fee_payer will be sent as a separate account when on-chain supports it. + /// Fee payer for rent top-ups. Not yet sent to on-chain (owner pays instead). + pub fee_payer: &'info AccountInfo, } impl<'info> ApproveCpi<'info> { @@ -47,6 +51,7 @@ impl<'info> ApproveCpi<'info> { let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + // Owner is writable (on-chain requires it — no fee_payer support yet) let account_metas = [ AccountMeta::writable(self.token_account.key()), AccountMeta::readonly(self.delegate.key()), diff --git a/sdk-libs/token-pinocchio/src/instruction/burn.rs b/sdk-libs/token-pinocchio/src/instruction/burn.rs index f0ae4453e7..789cbf1a5d 100644 --- a/sdk-libs/token-pinocchio/src/instruction/burn.rs +++ b/sdk-libs/token-pinocchio/src/instruction/burn.rs @@ -23,7 +23,7 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// amount: 100, /// authority: &ctx.accounts.authority, /// system_program: &ctx.accounts.system_program, -/// fee_payer: None, +/// fee_payer: &ctx.accounts.fee_payer, /// } /// .invoke()?; /// ``` @@ -33,8 +33,8 @@ pub struct BurnCpi<'info> { pub amount: u64, pub authority: &'info AccountInfo, pub system_program: &'info AccountInfo, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - pub fee_payer: Option<&'info AccountInfo>, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: &'info AccountInfo, } impl<'info> BurnCpi<'info> { @@ -43,74 +43,38 @@ impl<'info> BurnCpi<'info> { } pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { - // Build instruction data: discriminator(1) + amount(8) + optional max_top_up(2) - let mut data = [0u8; 11]; - data[0] = 8u8; // Burn discriminator + let mut data = [0u8; 9]; // discriminator(1) + amount(8) + data[0] = 8u8; data[1..9].copy_from_slice(&self.amount.to_le_bytes()); - let data_len = 9; - - // Authority is writable when no fee_payer is provided - let authority_writable = self.fee_payer.is_none(); let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); - if let Some(fee_payer) = self.fee_payer { - let account_metas = [ - AccountMeta::writable(self.source.key()), - AccountMeta::writable(self.mint.key()), - if authority_writable { - AccountMeta::writable_signer(self.authority.key()) - } else { - AccountMeta::readonly_signer(self.authority.key()) - }, - AccountMeta::readonly(self.system_program.key()), - AccountMeta::writable_signer(fee_payer.key()), - ]; + let account_metas = [ + AccountMeta::writable(self.source.key()), + AccountMeta::writable(self.mint.key()), + AccountMeta::readonly_signer(self.authority.key()), + AccountMeta::readonly(self.system_program.key()), + AccountMeta::writable_signer(self.fee_payer.key()), + ]; - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data[..data_len], - }; + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data, + }; - let account_infos = [ - self.source, - self.mint, - self.authority, - self.system_program, - fee_payer, - ]; + let account_infos = [ + self.source, + self.mint, + self.authority, + self.system_program, + self.fee_payer, + ]; - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) - } else { - slice_invoke_signed(&instruction, &account_infos, signers) - } + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) } else { - let account_metas = [ - AccountMeta::writable(self.source.key()), - AccountMeta::writable(self.mint.key()), - if authority_writable { - AccountMeta::writable_signer(self.authority.key()) - } else { - AccountMeta::readonly_signer(self.authority.key()) - }, - AccountMeta::readonly(self.system_program.key()), - ]; - - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data[..data_len], - }; - - let account_infos = [self.source, self.mint, self.authority, self.system_program]; - - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) - } else { - slice_invoke_signed(&instruction, &account_infos, signers) - } + slice_invoke_signed(&instruction, &account_infos, signers) } } } diff --git a/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs b/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs index 837925ca32..7253975341 100644 --- a/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs +++ b/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs @@ -24,7 +24,7 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// decimals: 9, /// authority: &ctx.accounts.authority, /// system_program: &ctx.accounts.system_program, -/// fee_payer: None, +/// fee_payer: &ctx.accounts.fee_payer, /// } /// .invoke()?; /// ``` @@ -35,8 +35,8 @@ pub struct BurnCheckedCpi<'info> { pub decimals: u8, pub authority: &'info AccountInfo, pub system_program: &'info AccountInfo, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - pub fee_payer: Option<&'info AccountInfo>, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: &'info AccountInfo, } impl<'info> BurnCheckedCpi<'info> { @@ -45,75 +45,39 @@ impl<'info> BurnCheckedCpi<'info> { } pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { - // Build instruction data: discriminator(1) + amount(8) + decimals(1) + optional max_top_up(2) - let mut data = [0u8; 12]; - data[0] = 15u8; // BurnChecked discriminator + let mut data = [0u8; 10]; // discriminator(1) + amount(8) + decimals(1) + data[0] = 15u8; data[1..9].copy_from_slice(&self.amount.to_le_bytes()); data[9] = self.decimals; - let data_len = 10; - - // Authority is writable when no fee_payer is provided - let authority_writable = self.fee_payer.is_none(); let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); - if let Some(fee_payer) = self.fee_payer { - let account_metas = [ - AccountMeta::writable(self.source.key()), - AccountMeta::writable(self.mint.key()), - if authority_writable { - AccountMeta::writable_signer(self.authority.key()) - } else { - AccountMeta::readonly_signer(self.authority.key()) - }, - AccountMeta::readonly(self.system_program.key()), - AccountMeta::writable_signer(fee_payer.key()), - ]; + let account_metas = [ + AccountMeta::writable(self.source.key()), + AccountMeta::writable(self.mint.key()), + AccountMeta::readonly_signer(self.authority.key()), + AccountMeta::readonly(self.system_program.key()), + AccountMeta::writable_signer(self.fee_payer.key()), + ]; - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data[..data_len], - }; + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data, + }; - let account_infos = [ - self.source, - self.mint, - self.authority, - self.system_program, - fee_payer, - ]; + let account_infos = [ + self.source, + self.mint, + self.authority, + self.system_program, + self.fee_payer, + ]; - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) - } else { - slice_invoke_signed(&instruction, &account_infos, signers) - } + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) } else { - let account_metas = [ - AccountMeta::writable(self.source.key()), - AccountMeta::writable(self.mint.key()), - if authority_writable { - AccountMeta::writable_signer(self.authority.key()) - } else { - AccountMeta::readonly_signer(self.authority.key()) - }, - AccountMeta::readonly(self.system_program.key()), - ]; - - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data[..data_len], - }; - - let account_infos = [self.source, self.mint, self.authority, self.system_program]; - - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) - } else { - slice_invoke_signed(&instruction, &account_infos, signers) - } + slice_invoke_signed(&instruction, &account_infos, signers) } } } diff --git a/sdk-libs/token-pinocchio/src/instruction/mint_to.rs b/sdk-libs/token-pinocchio/src/instruction/mint_to.rs index 6982e58937..978033f73a 100644 --- a/sdk-libs/token-pinocchio/src/instruction/mint_to.rs +++ b/sdk-libs/token-pinocchio/src/instruction/mint_to.rs @@ -23,7 +23,7 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// amount: 100, /// authority: &ctx.accounts.authority, /// system_program: &ctx.accounts.system_program, -/// fee_payer: None, +/// fee_payer: &ctx.accounts.fee_payer, /// } /// .invoke()?; /// ``` @@ -33,8 +33,8 @@ pub struct MintToCpi<'info> { pub amount: u64, pub authority: &'info AccountInfo, pub system_program: &'info AccountInfo, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - pub fee_payer: Option<&'info AccountInfo>, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: &'info AccountInfo, } impl<'info> MintToCpi<'info> { @@ -43,79 +43,38 @@ impl<'info> MintToCpi<'info> { } pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { - // Build instruction data: discriminator(1) + amount(8) + optional max_top_up(2) - let mut data = [0u8; 11]; - data[0] = 7u8; // MintTo discriminator + let mut data = [0u8; 9]; // discriminator(1) + amount(8) + data[0] = 7u8; data[1..9].copy_from_slice(&self.amount.to_le_bytes()); - let data_len = 9; - - // Authority is writable when no fee_payer is provided - let authority_writable = self.fee_payer.is_none(); let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); - if let Some(fee_payer) = self.fee_payer { - let account_metas = [ - AccountMeta::writable(self.mint.key()), - AccountMeta::writable(self.destination.key()), - if authority_writable { - AccountMeta::writable_signer(self.authority.key()) - } else { - AccountMeta::readonly_signer(self.authority.key()) - }, - AccountMeta::readonly(self.system_program.key()), - AccountMeta::writable_signer(fee_payer.key()), - ]; + let account_metas = [ + AccountMeta::writable(self.mint.key()), + AccountMeta::writable(self.destination.key()), + AccountMeta::readonly_signer(self.authority.key()), + AccountMeta::readonly(self.system_program.key()), + AccountMeta::writable_signer(self.fee_payer.key()), + ]; - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data[..data_len], - }; + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data, + }; - let account_infos = [ - self.mint, - self.destination, - self.authority, - self.system_program, - fee_payer, - ]; + let account_infos = [ + self.mint, + self.destination, + self.authority, + self.system_program, + self.fee_payer, + ]; - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) - } else { - slice_invoke_signed(&instruction, &account_infos, signers) - } + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) } else { - let account_metas = [ - AccountMeta::writable(self.mint.key()), - AccountMeta::writable(self.destination.key()), - if authority_writable { - AccountMeta::writable_signer(self.authority.key()) - } else { - AccountMeta::readonly_signer(self.authority.key()) - }, - AccountMeta::readonly(self.system_program.key()), - ]; - - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data[..data_len], - }; - - let account_infos = [ - self.mint, - self.destination, - self.authority, - self.system_program, - ]; - - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) - } else { - slice_invoke_signed(&instruction, &account_infos, signers) - } + slice_invoke_signed(&instruction, &account_infos, signers) } } } diff --git a/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs b/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs index b0e692e820..60602e2f1c 100644 --- a/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs +++ b/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs @@ -24,7 +24,7 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// decimals: 9, /// authority: &ctx.accounts.authority, /// system_program: &ctx.accounts.system_program, -/// fee_payer: None, +/// fee_payer: &ctx.accounts.fee_payer, /// } /// .invoke()?; /// ``` @@ -35,8 +35,8 @@ pub struct MintToCheckedCpi<'info> { pub decimals: u8, pub authority: &'info AccountInfo, pub system_program: &'info AccountInfo, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - pub fee_payer: Option<&'info AccountInfo>, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: &'info AccountInfo, } impl<'info> MintToCheckedCpi<'info> { @@ -45,80 +45,39 @@ impl<'info> MintToCheckedCpi<'info> { } pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { - // Build instruction data: discriminator(1) + amount(8) + decimals(1) + optional max_top_up(2) - let mut data = [0u8; 12]; - data[0] = 14u8; // MintToChecked discriminator + let mut data = [0u8; 10]; // discriminator(1) + amount(8) + decimals(1) + data[0] = 14u8; data[1..9].copy_from_slice(&self.amount.to_le_bytes()); data[9] = self.decimals; - let data_len = 10; - - // Authority is writable when no fee_payer is provided - let authority_writable = self.fee_payer.is_none(); let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); - if let Some(fee_payer) = self.fee_payer { - let account_metas = [ - AccountMeta::writable(self.mint.key()), - AccountMeta::writable(self.destination.key()), - if authority_writable { - AccountMeta::writable_signer(self.authority.key()) - } else { - AccountMeta::readonly_signer(self.authority.key()) - }, - AccountMeta::readonly(self.system_program.key()), - AccountMeta::writable_signer(fee_payer.key()), - ]; + let account_metas = [ + AccountMeta::writable(self.mint.key()), + AccountMeta::writable(self.destination.key()), + AccountMeta::readonly_signer(self.authority.key()), + AccountMeta::readonly(self.system_program.key()), + AccountMeta::writable_signer(self.fee_payer.key()), + ]; - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data[..data_len], - }; + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data, + }; - let account_infos = [ - self.mint, - self.destination, - self.authority, - self.system_program, - fee_payer, - ]; + let account_infos = [ + self.mint, + self.destination, + self.authority, + self.system_program, + self.fee_payer, + ]; - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) - } else { - slice_invoke_signed(&instruction, &account_infos, signers) - } + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) } else { - let account_metas = [ - AccountMeta::writable(self.mint.key()), - AccountMeta::writable(self.destination.key()), - if authority_writable { - AccountMeta::writable_signer(self.authority.key()) - } else { - AccountMeta::readonly_signer(self.authority.key()) - }, - AccountMeta::readonly(self.system_program.key()), - ]; - - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data[..data_len], - }; - - let account_infos = [ - self.mint, - self.destination, - self.authority, - self.system_program, - ]; - - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) - } else { - slice_invoke_signed(&instruction, &account_infos, signers) - } + slice_invoke_signed(&instruction, &account_infos, signers) } } } diff --git a/sdk-libs/token-pinocchio/src/instruction/revoke.rs b/sdk-libs/token-pinocchio/src/instruction/revoke.rs index 1cbca61334..31fc305148 100644 --- a/sdk-libs/token-pinocchio/src/instruction/revoke.rs +++ b/sdk-libs/token-pinocchio/src/instruction/revoke.rs @@ -21,6 +21,7 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// token_account: &ctx.accounts.token_account, /// owner: &ctx.accounts.owner, /// system_program: &ctx.accounts.system_program, +/// fee_payer: &ctx.accounts.fee_payer, /// } /// .invoke()?; /// ``` @@ -28,6 +29,9 @@ pub struct RevokeCpi<'info> { pub token_account: &'info AccountInfo, pub owner: &'info AccountInfo, pub system_program: &'info AccountInfo, + // TODO: fee_payer will be sent as a separate account when on-chain supports it. + /// Fee payer for rent top-ups. Not yet sent to on-chain (owner pays instead). + pub fee_payer: &'info AccountInfo, } impl<'info> RevokeCpi<'info> { @@ -41,6 +45,7 @@ impl<'info> RevokeCpi<'info> { let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + // Owner is writable (on-chain requires it — no fee_payer support yet) let account_metas = [ AccountMeta::writable(self.token_account.key()), AccountMeta::writable_signer(self.owner.key()), diff --git a/sdk-libs/token-pinocchio/src/instruction/transfer.rs b/sdk-libs/token-pinocchio/src/instruction/transfer.rs index b9ab960a6e..62c41f5f18 100644 --- a/sdk-libs/token-pinocchio/src/instruction/transfer.rs +++ b/sdk-libs/token-pinocchio/src/instruction/transfer.rs @@ -23,7 +23,7 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// amount: 100, /// authority: &ctx.accounts.authority, /// system_program: &ctx.accounts.system_program, -/// fee_payer: None, +/// fee_payer: &ctx.accounts.fee_payer, /// } /// .invoke()?; /// ``` @@ -33,8 +33,8 @@ pub struct TransferCpi<'info> { pub amount: u64, pub authority: &'info AccountInfo, pub system_program: &'info AccountInfo, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - pub fee_payer: Option<&'info AccountInfo>, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: &'info AccountInfo, } impl<'info> TransferCpi<'info> { @@ -43,79 +43,38 @@ impl<'info> TransferCpi<'info> { } pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { - // Build instruction data - let mut data = [0u8; 11]; // discriminator(1) + amount(8) + optional max_top_up(2) - data[0] = 3u8; // Transfer discriminator + let mut data = [0u8; 9]; // discriminator(1) + amount(8) + data[0] = 3u8; data[1..9].copy_from_slice(&self.amount.to_le_bytes()); - let data_len = 9; - - // Authority is writable when no fee_payer is provided - let authority_writable = self.fee_payer.is_none(); let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); - if let Some(fee_payer) = self.fee_payer { - let account_metas = [ - AccountMeta::writable(self.source.key()), - AccountMeta::writable(self.destination.key()), - if authority_writable { - AccountMeta::writable_signer(self.authority.key()) - } else { - AccountMeta::readonly_signer(self.authority.key()) - }, - AccountMeta::readonly(self.system_program.key()), - AccountMeta::writable_signer(fee_payer.key()), - ]; + let account_metas = [ + AccountMeta::writable(self.source.key()), + AccountMeta::writable(self.destination.key()), + AccountMeta::readonly_signer(self.authority.key()), + AccountMeta::readonly(self.system_program.key()), + AccountMeta::writable_signer(self.fee_payer.key()), + ]; - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data[..data_len], - }; + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data, + }; - let account_infos = [ - self.source, - self.destination, - self.authority, - self.system_program, - fee_payer, - ]; + let account_infos = [ + self.source, + self.destination, + self.authority, + self.system_program, + self.fee_payer, + ]; - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) - } else { - slice_invoke_signed(&instruction, &account_infos, signers) - } + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) } else { - let account_metas = [ - AccountMeta::writable(self.source.key()), - AccountMeta::writable(self.destination.key()), - if authority_writable { - AccountMeta::writable_signer(self.authority.key()) - } else { - AccountMeta::readonly_signer(self.authority.key()) - }, - AccountMeta::readonly(self.system_program.key()), - ]; - - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data[..data_len], - }; - - let account_infos = [ - self.source, - self.destination, - self.authority, - self.system_program, - ]; - - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) - } else { - slice_invoke_signed(&instruction, &account_infos, signers) - } + slice_invoke_signed(&instruction, &account_infos, signers) } } } diff --git a/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs b/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs index 7fe0f4e2d7..ee123d480f 100644 --- a/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs +++ b/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs @@ -132,7 +132,7 @@ impl<'info> TransferInterfaceCpi<'info> { amount: self.amount, authority: self.authority, system_program: self.system_program, - fee_payer: Some(self.payer), + fee_payer: self.payer, } .invoke(), @@ -228,7 +228,7 @@ impl<'info> TransferInterfaceCpi<'info> { amount: self.amount, authority: self.authority, system_program: self.system_program, - fee_payer: Some(self.payer), + fee_payer: self.payer, } .invoke_signed(signers), diff --git a/sdk-libs/token-pinocchio/src/lib.rs b/sdk-libs/token-pinocchio/src/lib.rs index 452e3641d6..d806db0ef6 100644 --- a/sdk-libs/token-pinocchio/src/lib.rs +++ b/sdk-libs/token-pinocchio/src/lib.rs @@ -35,7 +35,7 @@ //! amount: 100, //! authority: &ctx.accounts.authority, //! system_program: &ctx.accounts.system_program, -//! fee_payer: None, +//! fee_payer: &ctx.accounts.fee_payer, //! } //! .invoke()?; //! ``` diff --git a/sdk-libs/token-sdk/src/instruction/approve.rs b/sdk-libs/token-sdk/src/instruction/approve.rs index b1c6a8cff1..059138bed4 100644 --- a/sdk-libs/token-sdk/src/instruction/approve.rs +++ b/sdk-libs/token-sdk/src/instruction/approve.rs @@ -12,11 +12,13 @@ use solana_pubkey::Pubkey; /// # let token_account = Pubkey::new_unique(); /// # let delegate = Pubkey::new_unique(); /// # let owner = Pubkey::new_unique(); +/// # let fee_payer = Pubkey::new_unique(); /// let instruction = Approve { /// token_account, /// delegate, /// owner, /// amount: 100, +/// fee_payer, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` @@ -25,10 +27,14 @@ pub struct Approve { pub token_account: Pubkey, /// Delegate to approve pub delegate: Pubkey, - /// Owner of the Light Token account (signer, payer for top-up) + /// Owner of the Light Token account (signer, writable — on-chain requires owner to pay) pub owner: Pubkey, /// Amount of tokens to delegate pub amount: u64, + // TODO: fee_payer will be sent as a separate account when on-chain supports it. + // Currently owner pays for top-ups directly. + /// Fee payer for rent top-ups. Not yet sent to on-chain (owner pays instead). + pub fee_payer: Pubkey, } /// # Approve Light Token via CPI: @@ -39,12 +45,14 @@ pub struct Approve { /// # let delegate: AccountInfo = todo!(); /// # let owner: AccountInfo = todo!(); /// # let system_program: AccountInfo = todo!(); +/// # let fee_payer: AccountInfo = todo!(); /// ApproveCpi { /// token_account, /// delegate, /// owner, /// system_program, /// amount: 100, +/// fee_payer, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -55,6 +63,9 @@ pub struct ApproveCpi<'info> { pub owner: AccountInfo<'info>, pub system_program: AccountInfo<'info>, pub amount: u64, + // TODO: fee_payer will be sent as a separate account when on-chain supports it. + /// Fee payer for rent top-ups. Not yet sent to on-chain (owner pays instead). + pub fee_payer: AccountInfo<'info>, } impl<'info> ApproveCpi<'info> { @@ -92,6 +103,7 @@ impl<'info> From<&ApproveCpi<'info>> for Approve { delegate: *cpi.delegate.key, owner: *cpi.owner.key, amount: cpi.amount, + fee_payer: *cpi.fee_payer.key, } } } @@ -103,6 +115,7 @@ impl Approve { Ok(Instruction { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), + // Owner is writable (on-chain requires it — no fee_payer support yet) accounts: vec![ AccountMeta::new(self.token_account, false), AccountMeta::new_readonly(self.delegate, false), diff --git a/sdk-libs/token-sdk/src/instruction/burn.rs b/sdk-libs/token-sdk/src/instruction/burn.rs index d608c78910..257e59420b 100644 --- a/sdk-libs/token-sdk/src/instruction/burn.rs +++ b/sdk-libs/token-sdk/src/instruction/burn.rs @@ -12,12 +12,13 @@ use solana_pubkey::Pubkey; /// # let source = Pubkey::new_unique(); /// # let mint = Pubkey::new_unique(); /// # let authority = Pubkey::new_unique(); +/// # let fee_payer = Pubkey::new_unique(); /// let instruction = Burn { /// source, /// mint, /// amount: 100, /// authority, -/// fee_payer: None, +/// fee_payer, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` @@ -30,8 +31,8 @@ pub struct Burn { pub amount: u64, /// Owner of the Light Token account pub authority: Pubkey, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - pub fee_payer: Option, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: Pubkey, } /// # Burn ctoken via CPI: @@ -42,13 +43,14 @@ pub struct Burn { /// # let mint: AccountInfo = todo!(); /// # let authority: AccountInfo = todo!(); /// # let system_program: AccountInfo = todo!(); +/// # let fee_payer: AccountInfo = todo!(); /// BurnCpi { /// source, /// mint, /// amount: 100, /// authority, /// system_program, -/// fee_payer: None, +/// fee_payer, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -59,8 +61,8 @@ pub struct BurnCpi<'info> { pub amount: u64, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - pub fee_payer: Option>, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: AccountInfo<'info>, } impl<'info> BurnCpi<'info> { @@ -70,36 +72,26 @@ impl<'info> BurnCpi<'info> { pub fn invoke(self) -> Result<(), ProgramError> { let instruction = Burn::from(&self).instruction()?; - if let Some(fee_payer) = self.fee_payer { - let account_infos = [ - self.source, - self.mint, - self.authority, - self.system_program, - fee_payer, - ]; - invoke(&instruction, &account_infos) - } else { - let account_infos = [self.source, self.mint, self.authority, self.system_program]; - invoke(&instruction, &account_infos) - } + let account_infos = [ + self.source, + self.mint, + self.authority, + self.system_program, + self.fee_payer, + ]; + invoke(&instruction, &account_infos) } pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { let instruction = Burn::from(&self).instruction()?; - if let Some(fee_payer) = self.fee_payer { - let account_infos = [ - self.source, - self.mint, - self.authority, - self.system_program, - fee_payer, - ]; - invoke_signed(&instruction, &account_infos, signer_seeds) - } else { - let account_infos = [self.source, self.mint, self.authority, self.system_program]; - invoke_signed(&instruction, &account_infos, signer_seeds) - } + let account_infos = [ + self.source, + self.mint, + self.authority, + self.system_program, + self.fee_payer, + ]; + invoke_signed(&instruction, &account_infos, signer_seeds) } } @@ -110,41 +102,53 @@ impl<'info> From<&BurnCpi<'info>> for Burn { mint: *cpi.mint.key, amount: cpi.amount, authority: *cpi.authority.key, - fee_payer: cpi.fee_payer.as_ref().map(|a| *a.key), + fee_payer: *cpi.fee_payer.key, } } } impl Burn { + pub fn with_max_top_up(self, max_top_up: u16) -> BurnWithTopUp { + BurnWithTopUp { + inner: self, + max_top_up, + } + } + pub fn instruction(self) -> Result { - // Authority is writable only when no fee_payer (authority pays for top-ups) - let authority_meta = if self.fee_payer.is_none() { - AccountMeta::new(self.authority, true) - } else { - AccountMeta::new_readonly(self.authority, true) - }; + self.build_instruction(None) + } - let mut accounts = vec![ + fn build_instruction(self, max_top_up: Option) -> Result { + let accounts = vec![ AccountMeta::new(self.source, false), AccountMeta::new(self.mint, false), - authority_meta, - // System program required for rent top-up CPIs + AccountMeta::new_readonly(self.authority, true), AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new(self.fee_payer, true), ]; - // Add fee_payer if provided (must be signer and writable) - if let Some(fee_payer) = self.fee_payer { - accounts.push(AccountMeta::new(fee_payer, true)); + let mut data = vec![8u8]; + data.extend_from_slice(&self.amount.to_le_bytes()); + if let Some(max_top_up) = max_top_up { + data.extend_from_slice(&max_top_up.to_le_bytes()); } Ok(Instruction { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), accounts, - data: { - let mut data = vec![8u8]; // CTokenBurn discriminator - data.extend_from_slice(&self.amount.to_le_bytes()); - data - }, + data, }) } } + +pub struct BurnWithTopUp { + inner: Burn, + max_top_up: u16, +} + +impl BurnWithTopUp { + pub fn instruction(self) -> Result { + self.inner.build_instruction(Some(self.max_top_up)) + } +} diff --git a/sdk-libs/token-sdk/src/instruction/burn_checked.rs b/sdk-libs/token-sdk/src/instruction/burn_checked.rs index 393499a13b..b3e2e4ff1c 100644 --- a/sdk-libs/token-sdk/src/instruction/burn_checked.rs +++ b/sdk-libs/token-sdk/src/instruction/burn_checked.rs @@ -12,13 +12,14 @@ use solana_pubkey::Pubkey; /// # let source = Pubkey::new_unique(); /// # let mint = Pubkey::new_unique(); /// # let authority = Pubkey::new_unique(); +/// # let fee_payer = Pubkey::new_unique(); /// let instruction = BurnChecked { /// source, /// mint, /// amount: 100, /// decimals: 8, /// authority, -/// fee_payer: None, +/// fee_payer, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` @@ -33,8 +34,8 @@ pub struct BurnChecked { pub decimals: u8, /// Owner of the Light Token account pub authority: Pubkey, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - pub fee_payer: Option, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: Pubkey, } /// # Burn ctoken via CPI with decimals validation: @@ -45,6 +46,7 @@ pub struct BurnChecked { /// # let mint: AccountInfo = todo!(); /// # let authority: AccountInfo = todo!(); /// # let system_program: AccountInfo = todo!(); +/// # let fee_payer: AccountInfo = todo!(); /// BurnCheckedCpi { /// source, /// mint, @@ -52,7 +54,7 @@ pub struct BurnChecked { /// decimals: 8, /// authority, /// system_program, -/// fee_payer: None, +/// fee_payer, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -64,8 +66,8 @@ pub struct BurnCheckedCpi<'info> { pub decimals: u8, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - pub fee_payer: Option>, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: AccountInfo<'info>, } impl<'info> BurnCheckedCpi<'info> { @@ -75,36 +77,26 @@ impl<'info> BurnCheckedCpi<'info> { pub fn invoke(self) -> Result<(), ProgramError> { let instruction = BurnChecked::from(&self).instruction()?; - if let Some(fee_payer) = self.fee_payer { - let account_infos = [ - self.source, - self.mint, - self.authority, - self.system_program, - fee_payer, - ]; - invoke(&instruction, &account_infos) - } else { - let account_infos = [self.source, self.mint, self.authority, self.system_program]; - invoke(&instruction, &account_infos) - } + let account_infos = [ + self.source, + self.mint, + self.authority, + self.system_program, + self.fee_payer, + ]; + invoke(&instruction, &account_infos) } pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { let instruction = BurnChecked::from(&self).instruction()?; - if let Some(fee_payer) = self.fee_payer { - let account_infos = [ - self.source, - self.mint, - self.authority, - self.system_program, - fee_payer, - ]; - invoke_signed(&instruction, &account_infos, signer_seeds) - } else { - let account_infos = [self.source, self.mint, self.authority, self.system_program]; - invoke_signed(&instruction, &account_infos, signer_seeds) - } + let account_infos = [ + self.source, + self.mint, + self.authority, + self.system_program, + self.fee_payer, + ]; + invoke_signed(&instruction, &account_infos, signer_seeds) } } @@ -116,42 +108,54 @@ impl<'info> From<&BurnCheckedCpi<'info>> for BurnChecked { amount: cpi.amount, decimals: cpi.decimals, authority: *cpi.authority.key, - fee_payer: cpi.fee_payer.as_ref().map(|a| *a.key), + fee_payer: *cpi.fee_payer.key, } } } impl BurnChecked { + pub fn with_max_top_up(self, max_top_up: u16) -> BurnCheckedWithTopUp { + BurnCheckedWithTopUp { + inner: self, + max_top_up, + } + } + pub fn instruction(self) -> Result { - // Authority is writable only when no fee_payer (authority pays for top-ups) - let authority_meta = if self.fee_payer.is_none() { - AccountMeta::new(self.authority, true) - } else { - AccountMeta::new_readonly(self.authority, true) - }; + self.build_instruction(None) + } - let mut accounts = vec![ + fn build_instruction(self, max_top_up: Option) -> Result { + let accounts = vec![ AccountMeta::new(self.source, false), AccountMeta::new(self.mint, false), - authority_meta, - // System program required for rent top-up CPIs + AccountMeta::new_readonly(self.authority, true), AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new(self.fee_payer, true), ]; - // Add fee_payer if provided (must be signer and writable) - if let Some(fee_payer) = self.fee_payer { - accounts.push(AccountMeta::new(fee_payer, true)); + let mut data = vec![15u8]; + data.extend_from_slice(&self.amount.to_le_bytes()); + data.push(self.decimals); + if let Some(max_top_up) = max_top_up { + data.extend_from_slice(&max_top_up.to_le_bytes()); } Ok(Instruction { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), accounts, - data: { - let mut data = vec![15u8]; // CTokenBurnChecked discriminator - data.extend_from_slice(&self.amount.to_le_bytes()); - data.push(self.decimals); - data - }, + data, }) } } + +pub struct BurnCheckedWithTopUp { + inner: BurnChecked, + max_top_up: u16, +} + +impl BurnCheckedWithTopUp { + pub fn instruction(self) -> Result { + self.inner.build_instruction(Some(self.max_top_up)) + } +} diff --git a/sdk-libs/token-sdk/src/instruction/mint_to.rs b/sdk-libs/token-sdk/src/instruction/mint_to.rs index ed03074087..029564ee13 100644 --- a/sdk-libs/token-sdk/src/instruction/mint_to.rs +++ b/sdk-libs/token-sdk/src/instruction/mint_to.rs @@ -12,12 +12,13 @@ use solana_pubkey::Pubkey; /// # let mint = Pubkey::new_unique(); /// # let destination = Pubkey::new_unique(); /// # let authority = Pubkey::new_unique(); +/// # let fee_payer = Pubkey::new_unique(); /// let instruction = MintTo { /// mint, /// destination, /// amount: 100, /// authority, -/// fee_payer: None, +/// fee_payer, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` @@ -30,8 +31,8 @@ pub struct MintTo { pub amount: u64, /// Mint authority pub authority: Pubkey, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - pub fee_payer: Option, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: Pubkey, } /// # Mint to ctoken via CPI: @@ -42,13 +43,14 @@ pub struct MintTo { /// # let destination: AccountInfo = todo!(); /// # let authority: AccountInfo = todo!(); /// # let system_program: AccountInfo = todo!(); +/// # let fee_payer: AccountInfo = todo!(); /// MintToCpi { /// mint, /// destination, /// amount: 100, /// authority, /// system_program, -/// fee_payer: None, +/// fee_payer, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -59,8 +61,8 @@ pub struct MintToCpi<'info> { pub amount: u64, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - pub fee_payer: Option>, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: AccountInfo<'info>, } impl<'info> MintToCpi<'info> { @@ -70,46 +72,26 @@ impl<'info> MintToCpi<'info> { pub fn invoke(self) -> Result<(), ProgramError> { let instruction = MintTo::from(&self).instruction()?; - if let Some(fee_payer) = self.fee_payer { - let account_infos = [ - self.mint, - self.destination, - self.authority, - self.system_program, - fee_payer, - ]; - invoke(&instruction, &account_infos) - } else { - let account_infos = [ - self.mint, - self.destination, - self.authority, - self.system_program, - ]; - invoke(&instruction, &account_infos) - } + let account_infos = [ + self.mint, + self.destination, + self.authority, + self.system_program, + self.fee_payer, + ]; + invoke(&instruction, &account_infos) } pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { let instruction = MintTo::from(&self).instruction()?; - if let Some(fee_payer) = self.fee_payer { - let account_infos = [ - self.mint, - self.destination, - self.authority, - self.system_program, - fee_payer, - ]; - invoke_signed(&instruction, &account_infos, signer_seeds) - } else { - let account_infos = [ - self.mint, - self.destination, - self.authority, - self.system_program, - ]; - invoke_signed(&instruction, &account_infos, signer_seeds) - } + let account_infos = [ + self.mint, + self.destination, + self.authority, + self.system_program, + self.fee_payer, + ]; + invoke_signed(&instruction, &account_infos, signer_seeds) } } @@ -120,41 +102,53 @@ impl<'info> From<&MintToCpi<'info>> for MintTo { destination: *cpi.destination.key, amount: cpi.amount, authority: *cpi.authority.key, - fee_payer: cpi.fee_payer.as_ref().map(|a| *a.key), + fee_payer: *cpi.fee_payer.key, } } } impl MintTo { + pub fn with_max_top_up(self, max_top_up: u16) -> MintToWithTopUp { + MintToWithTopUp { + inner: self, + max_top_up, + } + } + pub fn instruction(self) -> Result { - // Authority is writable only when no fee_payer (authority pays for top-ups) - let authority_meta = if self.fee_payer.is_none() { - AccountMeta::new(self.authority, true) - } else { - AccountMeta::new_readonly(self.authority, true) - }; + self.build_instruction(None) + } - let mut accounts = vec![ + fn build_instruction(self, max_top_up: Option) -> Result { + let accounts = vec![ AccountMeta::new(self.mint, false), AccountMeta::new(self.destination, false), - authority_meta, - // System program required for rent top-up CPIs + AccountMeta::new_readonly(self.authority, true), AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new(self.fee_payer, true), ]; - // Add fee_payer if provided (must be signer and writable) - if let Some(fee_payer) = self.fee_payer { - accounts.push(AccountMeta::new(fee_payer, true)); + let mut data = vec![7u8]; + data.extend_from_slice(&self.amount.to_le_bytes()); + if let Some(max_top_up) = max_top_up { + data.extend_from_slice(&max_top_up.to_le_bytes()); } Ok(Instruction { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), accounts, - data: { - let mut data = vec![7u8]; // MintTo discriminator - data.extend_from_slice(&self.amount.to_le_bytes()); - data - }, + data, }) } } + +pub struct MintToWithTopUp { + inner: MintTo, + max_top_up: u16, +} + +impl MintToWithTopUp { + pub fn instruction(self) -> Result { + self.inner.build_instruction(Some(self.max_top_up)) + } +} diff --git a/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs b/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs index e915d496bb..8d844ee74f 100644 --- a/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs +++ b/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs @@ -12,13 +12,14 @@ use solana_pubkey::Pubkey; /// # let mint = Pubkey::new_unique(); /// # let destination = Pubkey::new_unique(); /// # let authority = Pubkey::new_unique(); +/// # let fee_payer = Pubkey::new_unique(); /// let instruction = MintToChecked { /// mint, /// destination, /// amount: 100, /// decimals: 8, /// authority, -/// fee_payer: None, +/// fee_payer, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` @@ -33,8 +34,8 @@ pub struct MintToChecked { pub decimals: u8, /// Mint authority pub authority: Pubkey, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - pub fee_payer: Option, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: Pubkey, } /// # Mint to ctoken via CPI with decimals validation: @@ -45,6 +46,7 @@ pub struct MintToChecked { /// # let destination: AccountInfo = todo!(); /// # let authority: AccountInfo = todo!(); /// # let system_program: AccountInfo = todo!(); +/// # let fee_payer: AccountInfo = todo!(); /// MintToCheckedCpi { /// mint, /// destination, @@ -52,7 +54,7 @@ pub struct MintToChecked { /// decimals: 8, /// authority, /// system_program, -/// fee_payer: None, +/// fee_payer, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -64,8 +66,8 @@ pub struct MintToCheckedCpi<'info> { pub decimals: u8, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - pub fee_payer: Option>, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: AccountInfo<'info>, } impl<'info> MintToCheckedCpi<'info> { @@ -75,46 +77,26 @@ impl<'info> MintToCheckedCpi<'info> { pub fn invoke(self) -> Result<(), ProgramError> { let instruction = MintToChecked::from(&self).instruction()?; - if let Some(fee_payer) = self.fee_payer { - let account_infos = [ - self.mint, - self.destination, - self.authority, - self.system_program, - fee_payer, - ]; - invoke(&instruction, &account_infos) - } else { - let account_infos = [ - self.mint, - self.destination, - self.authority, - self.system_program, - ]; - invoke(&instruction, &account_infos) - } + let account_infos = [ + self.mint, + self.destination, + self.authority, + self.system_program, + self.fee_payer, + ]; + invoke(&instruction, &account_infos) } pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { let instruction = MintToChecked::from(&self).instruction()?; - if let Some(fee_payer) = self.fee_payer { - let account_infos = [ - self.mint, - self.destination, - self.authority, - self.system_program, - fee_payer, - ]; - invoke_signed(&instruction, &account_infos, signer_seeds) - } else { - let account_infos = [ - self.mint, - self.destination, - self.authority, - self.system_program, - ]; - invoke_signed(&instruction, &account_infos, signer_seeds) - } + let account_infos = [ + self.mint, + self.destination, + self.authority, + self.system_program, + self.fee_payer, + ]; + invoke_signed(&instruction, &account_infos, signer_seeds) } } @@ -126,42 +108,54 @@ impl<'info> From<&MintToCheckedCpi<'info>> for MintToChecked { amount: cpi.amount, decimals: cpi.decimals, authority: *cpi.authority.key, - fee_payer: cpi.fee_payer.as_ref().map(|a| *a.key), + fee_payer: *cpi.fee_payer.key, } } } impl MintToChecked { + pub fn with_max_top_up(self, max_top_up: u16) -> MintToCheckedWithTopUp { + MintToCheckedWithTopUp { + inner: self, + max_top_up, + } + } + pub fn instruction(self) -> Result { - // Authority is writable only when no fee_payer (authority pays for top-ups) - let authority_meta = if self.fee_payer.is_none() { - AccountMeta::new(self.authority, true) - } else { - AccountMeta::new_readonly(self.authority, true) - }; + self.build_instruction(None) + } - let mut accounts = vec![ + fn build_instruction(self, max_top_up: Option) -> Result { + let accounts = vec![ AccountMeta::new(self.mint, false), AccountMeta::new(self.destination, false), - authority_meta, - // System program required for rent top-up CPIs + AccountMeta::new_readonly(self.authority, true), AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new(self.fee_payer, true), ]; - // Add fee_payer if provided (must be signer and writable) - if let Some(fee_payer) = self.fee_payer { - accounts.push(AccountMeta::new(fee_payer, true)); + let mut data = vec![14u8]; + data.extend_from_slice(&self.amount.to_le_bytes()); + data.push(self.decimals); + if let Some(max_top_up) = max_top_up { + data.extend_from_slice(&max_top_up.to_le_bytes()); } Ok(Instruction { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), accounts, - data: { - let mut data = vec![14u8]; // TokenMintToChecked discriminator - data.extend_from_slice(&self.amount.to_le_bytes()); - data.push(self.decimals); - data - }, + data, }) } } + +pub struct MintToCheckedWithTopUp { + inner: MintToChecked, + max_top_up: u16, +} + +impl MintToCheckedWithTopUp { + pub fn instruction(self) -> Result { + self.inner.build_instruction(Some(self.max_top_up)) + } +} diff --git a/sdk-libs/token-sdk/src/instruction/revoke.rs b/sdk-libs/token-sdk/src/instruction/revoke.rs index d746dbe850..32aa497555 100644 --- a/sdk-libs/token-sdk/src/instruction/revoke.rs +++ b/sdk-libs/token-sdk/src/instruction/revoke.rs @@ -11,17 +11,23 @@ use solana_pubkey::Pubkey; /// # use light_token::instruction::Revoke; /// # let token_account = Pubkey::new_unique(); /// # let owner = Pubkey::new_unique(); +/// # let fee_payer = Pubkey::new_unique(); /// let instruction = Revoke { /// token_account, /// owner, +/// fee_payer, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` pub struct Revoke { /// Light Token account to revoke delegation for pub token_account: Pubkey, - /// Owner of the Light Token account (signer, payer for top-up) + /// Owner of the Light Token account (signer, writable — on-chain requires owner to pay) pub owner: Pubkey, + // TODO: fee_payer will be sent as a separate account when on-chain supports it. + // Currently owner pays for top-ups directly. + /// Fee payer for rent top-ups. Not yet sent to on-chain (owner pays instead). + pub fee_payer: Pubkey, } /// # Revoke Light Token via CPI: @@ -31,10 +37,12 @@ pub struct Revoke { /// # let token_account: AccountInfo = todo!(); /// # let owner: AccountInfo = todo!(); /// # let system_program: AccountInfo = todo!(); +/// # let fee_payer: AccountInfo = todo!(); /// RevokeCpi { /// token_account, /// owner, /// system_program, +/// fee_payer, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -43,6 +51,9 @@ pub struct RevokeCpi<'info> { pub token_account: AccountInfo<'info>, pub owner: AccountInfo<'info>, pub system_program: AccountInfo<'info>, + // TODO: fee_payer will be sent as a separate account when on-chain supports it. + /// Fee payer for rent top-ups. Not yet sent to on-chain (owner pays instead). + pub fee_payer: AccountInfo<'info>, } impl<'info> RevokeCpi<'info> { @@ -68,6 +79,7 @@ impl<'info> From<&RevokeCpi<'info>> for Revoke { Self { token_account: *cpi.token_account.key, owner: *cpi.owner.key, + fee_payer: *cpi.fee_payer.key, } } } @@ -76,6 +88,7 @@ impl Revoke { pub fn instruction(self) -> Result { Ok(Instruction { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), + // Owner is writable (on-chain requires it — no fee_payer support yet) accounts: vec![ AccountMeta::new(self.token_account, false), AccountMeta::new(self.owner, true), diff --git a/sdk-libs/token-sdk/src/instruction/transfer.rs b/sdk-libs/token-sdk/src/instruction/transfer.rs index f4390f52ed..a9cd5187af 100644 --- a/sdk-libs/token-sdk/src/instruction/transfer.rs +++ b/sdk-libs/token-sdk/src/instruction/transfer.rs @@ -12,12 +12,13 @@ use solana_pubkey::Pubkey; /// # let source = Pubkey::new_unique(); /// # let destination = Pubkey::new_unique(); /// # let authority = Pubkey::new_unique(); +/// # let fee_payer = Pubkey::new_unique(); /// let instruction = Transfer { /// source, /// destination, /// amount: 100, /// authority, -/// fee_payer: None, +/// fee_payer, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` @@ -26,9 +27,8 @@ pub struct Transfer { pub destination: Pubkey, pub amount: u64, pub authority: Pubkey, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - /// When set, fee_payer pays for top-ups instead of authority. - pub fee_payer: Option, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: Pubkey, } /// # Transfer ctoken via CPI: @@ -39,13 +39,14 @@ pub struct Transfer { /// # let destination: AccountInfo = todo!(); /// # let authority: AccountInfo = todo!(); /// # let system_program: AccountInfo = todo!(); +/// # let fee_payer: AccountInfo = todo!(); /// TransferCpi { /// source, /// destination, /// amount: 100, /// authority, /// system_program, -/// fee_payer: None, +/// fee_payer, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -56,8 +57,8 @@ pub struct TransferCpi<'info> { pub amount: u64, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - pub fee_payer: Option>, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: AccountInfo<'info>, } impl<'info> TransferCpi<'info> { @@ -67,46 +68,26 @@ impl<'info> TransferCpi<'info> { pub fn invoke(self) -> Result<(), ProgramError> { let instruction = Transfer::from(&self).instruction()?; - if let Some(fee_payer) = self.fee_payer { - let account_infos = [ - self.source, - self.destination, - self.authority, - self.system_program, - fee_payer, - ]; - invoke(&instruction, &account_infos) - } else { - let account_infos = [ - self.source, - self.destination, - self.authority, - self.system_program, - ]; - invoke(&instruction, &account_infos) - } + let account_infos = [ + self.source, + self.destination, + self.authority, + self.system_program, + self.fee_payer, + ]; + invoke(&instruction, &account_infos) } pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { let instruction = Transfer::from(&self).instruction()?; - if let Some(fee_payer) = self.fee_payer { - let account_infos = [ - self.source, - self.destination, - self.authority, - self.system_program, - fee_payer, - ]; - invoke_signed(&instruction, &account_infos, signer_seeds) - } else { - let account_infos = [ - self.source, - self.destination, - self.authority, - self.system_program, - ]; - invoke_signed(&instruction, &account_infos, signer_seeds) - } + let account_infos = [ + self.source, + self.destination, + self.authority, + self.system_program, + self.fee_payer, + ]; + invoke_signed(&instruction, &account_infos, signer_seeds) } } @@ -117,41 +98,53 @@ impl<'info> From<&TransferCpi<'info>> for Transfer { destination: *account_infos.destination.key, amount: account_infos.amount, authority: *account_infos.authority.key, - fee_payer: account_infos.fee_payer.as_ref().map(|a| *a.key), + fee_payer: *account_infos.fee_payer.key, } } } impl Transfer { + pub fn with_max_top_up(self, max_top_up: u16) -> TransferWithTopUp { + TransferWithTopUp { + inner: self, + max_top_up, + } + } + pub fn instruction(self) -> Result { - // Authority is writable only when no fee_payer (authority pays for top-ups) - let authority_meta = if self.fee_payer.is_none() { - AccountMeta::new(self.authority, true) - } else { - AccountMeta::new_readonly(self.authority, true) - }; + self.build_instruction(None) + } - let mut accounts = vec![ + fn build_instruction(self, max_top_up: Option) -> Result { + let accounts = vec![ AccountMeta::new(self.source, false), AccountMeta::new(self.destination, false), - authority_meta, - // System program required for rent top-up CPIs + AccountMeta::new_readonly(self.authority, true), AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new(self.fee_payer, true), ]; - // Add fee_payer if provided (must be signer and writable) - if let Some(fee_payer) = self.fee_payer { - accounts.push(AccountMeta::new(fee_payer, true)); + let mut data = vec![3u8]; + data.extend_from_slice(&self.amount.to_le_bytes()); + if let Some(max_top_up) = max_top_up { + data.extend_from_slice(&max_top_up.to_le_bytes()); } Ok(Instruction { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), accounts, - data: { - let mut data = vec![3u8]; - data.extend_from_slice(&self.amount.to_le_bytes()); - data - }, + data, }) } } + +pub struct TransferWithTopUp { + inner: Transfer, + max_top_up: u16, +} + +impl TransferWithTopUp { + pub fn instruction(self) -> Result { + self.inner.build_instruction(Some(self.max_top_up)) + } +} diff --git a/sdk-libs/token-sdk/src/instruction/transfer_checked.rs b/sdk-libs/token-sdk/src/instruction/transfer_checked.rs index d21daffbf0..0199526d6e 100644 --- a/sdk-libs/token-sdk/src/instruction/transfer_checked.rs +++ b/sdk-libs/token-sdk/src/instruction/transfer_checked.rs @@ -13,6 +13,7 @@ use solana_pubkey::Pubkey; /// # let mint = Pubkey::new_unique(); /// # let destination = Pubkey::new_unique(); /// # let authority = Pubkey::new_unique(); +/// # let fee_payer = Pubkey::new_unique(); /// let instruction = TransferChecked { /// source, /// mint, @@ -20,7 +21,7 @@ use solana_pubkey::Pubkey; /// amount: 100, /// decimals: 9, /// authority, -/// fee_payer: None, +/// fee_payer, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` @@ -31,8 +32,8 @@ pub struct TransferChecked { pub amount: u64, pub decimals: u8, pub authority: Pubkey, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - pub fee_payer: Option, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: Pubkey, } /// # Transfer ctoken checked via CPI: @@ -44,6 +45,7 @@ pub struct TransferChecked { /// # let destination: AccountInfo = todo!(); /// # let authority: AccountInfo = todo!(); /// # let system_program: AccountInfo = todo!(); +/// # let fee_payer: AccountInfo = todo!(); /// TransferCheckedCpi { /// source, /// mint, @@ -52,7 +54,7 @@ pub struct TransferChecked { /// decimals: 9, /// authority, /// system_program, -/// fee_payer: None, +/// fee_payer, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -65,8 +67,8 @@ pub struct TransferCheckedCpi<'info> { pub decimals: u8, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - pub fee_payer: Option>, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: AccountInfo<'info>, } impl<'info> TransferCheckedCpi<'info> { @@ -76,50 +78,28 @@ impl<'info> TransferCheckedCpi<'info> { pub fn invoke(self) -> Result<(), ProgramError> { let instruction = TransferChecked::from(&self).instruction()?; - if let Some(fee_payer) = self.fee_payer { - let account_infos = [ - self.source, - self.mint, - self.destination, - self.authority, - self.system_program, - fee_payer, - ]; - invoke(&instruction, &account_infos) - } else { - let account_infos = [ - self.source, - self.mint, - self.destination, - self.authority, - self.system_program, - ]; - invoke(&instruction, &account_infos) - } + let account_infos = [ + self.source, + self.mint, + self.destination, + self.authority, + self.system_program, + self.fee_payer, + ]; + invoke(&instruction, &account_infos) } pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { let instruction = TransferChecked::from(&self).instruction()?; - if let Some(fee_payer) = self.fee_payer { - let account_infos = [ - self.source, - self.mint, - self.destination, - self.authority, - self.system_program, - fee_payer, - ]; - invoke_signed(&instruction, &account_infos, signer_seeds) - } else { - let account_infos = [ - self.source, - self.mint, - self.destination, - self.authority, - self.system_program, - ]; - invoke_signed(&instruction, &account_infos, signer_seeds) - } + let account_infos = [ + self.source, + self.mint, + self.destination, + self.authority, + self.system_program, + self.fee_payer, + ]; + invoke_signed(&instruction, &account_infos, signer_seeds) } } @@ -132,43 +112,55 @@ impl<'info> From<&TransferCheckedCpi<'info>> for TransferChecked { amount: account_infos.amount, decimals: account_infos.decimals, authority: *account_infos.authority.key, - fee_payer: account_infos.fee_payer.as_ref().map(|a| *a.key), + fee_payer: *account_infos.fee_payer.key, } } } impl TransferChecked { + pub fn with_max_top_up(self, max_top_up: u16) -> TransferCheckedWithTopUp { + TransferCheckedWithTopUp { + inner: self, + max_top_up, + } + } + pub fn instruction(self) -> Result { - // Authority is writable only when no fee_payer (authority pays for top-ups) - let authority_meta = if self.fee_payer.is_none() { - AccountMeta::new(self.authority, true) - } else { - AccountMeta::new_readonly(self.authority, true) - }; + self.build_instruction(None) + } - let mut accounts = vec![ + fn build_instruction(self, max_top_up: Option) -> Result { + let accounts = vec![ AccountMeta::new(self.source, false), AccountMeta::new_readonly(self.mint, false), AccountMeta::new(self.destination, false), - authority_meta, - // System program required for rent top-up CPIs + AccountMeta::new_readonly(self.authority, true), AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new(self.fee_payer, true), ]; - // Add fee_payer if provided (must be signer and writable) - if let Some(fee_payer) = self.fee_payer { - accounts.push(AccountMeta::new(fee_payer, true)); + let mut data = vec![12u8]; + data.extend_from_slice(&self.amount.to_le_bytes()); + data.push(self.decimals); + if let Some(max_top_up) = max_top_up { + data.extend_from_slice(&max_top_up.to_le_bytes()); } Ok(Instruction { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), accounts, - data: { - let mut data = vec![12u8]; // TransferChecked discriminator (SPL compatible) - data.extend_from_slice(&self.amount.to_le_bytes()); - data.push(self.decimals); - data - }, + data, }) } } + +pub struct TransferCheckedWithTopUp { + inner: TransferChecked, + max_top_up: u16, +} + +impl TransferCheckedWithTopUp { + pub fn instruction(self) -> Result { + self.inner.build_instruction(Some(self.max_top_up)) + } +} diff --git a/sdk-libs/token-sdk/src/instruction/transfer_interface.rs b/sdk-libs/token-sdk/src/instruction/transfer_interface.rs index b4cb36b85a..218c3febd3 100644 --- a/sdk-libs/token-sdk/src/instruction/transfer_interface.rs +++ b/sdk-libs/token-sdk/src/instruction/transfer_interface.rs @@ -130,7 +130,7 @@ impl TransferInterface { destination: self.destination, amount: self.amount, authority: self.authority, - fee_payer: Some(self.payer), + fee_payer: self.payer, } .instruction(), diff --git a/sdk-libs/token-sdk/tests/instruction_transfer.rs b/sdk-libs/token-sdk/tests/instruction_transfer.rs index df829bf976..459b3d53bd 100644 --- a/sdk-libs/token-sdk/tests/instruction_transfer.rs +++ b/sdk-libs/token-sdk/tests/instruction_transfer.rs @@ -2,35 +2,33 @@ use light_token::instruction::{Transfer, LIGHT_TOKEN_PROGRAM_ID}; use solana_instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; -/// Test Transfer instruction without fee_payer. -/// Authority is writable signer since it pays for top-ups. -/// Short format: discriminator (1) + amount (8) = 9 bytes. +/// Test Transfer instruction (no max_top_up). +/// Authority is readonly signer; fee_payer is always writable signer. #[test] fn test_transfer_basic() { let source = Pubkey::new_from_array([1u8; 32]); let destination = Pubkey::new_from_array([2u8; 32]); let authority = Pubkey::new_from_array([3u8; 32]); + let fee_payer = Pubkey::new_from_array([4u8; 32]); let instruction = Transfer { source, destination, amount: 100, authority, - fee_payer: None, + fee_payer, } .instruction() .expect("Failed to create instruction"); - // Hardcoded expected instruction - // - authority is writable (no fee_payer -> authority pays for top-ups) - // - data: discriminator (3) + amount (8 bytes) = 9 bytes (short format, on-chain defaults max_top_up to u16::MAX) let expected = Instruction { program_id: LIGHT_TOKEN_PROGRAM_ID, accounts: vec![ - AccountMeta::new(source, false), // source: writable, not signer - AccountMeta::new(destination, false), // destination: writable, not signer - AccountMeta::new(authority, true), // authority: writable, signer (pays for top-ups) - AccountMeta::new_readonly(Pubkey::default(), false), // system_program: readonly, not signer + AccountMeta::new(source, false), + AccountMeta::new(destination, false), + AccountMeta::new_readonly(authority, true), + AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new(fee_payer, true), ], data: vec![ 3u8, // Transfer discriminator @@ -44,11 +42,10 @@ fn test_transfer_basic() { ); } -/// Test Transfer instruction with fee_payer set. -/// Fee_payer is added as 5th account. Authority is readonly. -/// Short format: discriminator (1) + amount (8) = 9 bytes. +/// Test Transfer instruction with max_top_up via builder. +/// max_top_up is appended as 2 extra bytes to instruction data. #[test] -fn test_transfer_with_fee_payer() { +fn test_transfer_with_max_top_up() { let source = Pubkey::new_from_array([1u8; 32]); let destination = Pubkey::new_from_array([2u8; 32]); let authority = Pubkey::new_from_array([3u8; 32]); @@ -59,32 +56,30 @@ fn test_transfer_with_fee_payer() { destination, amount: 100, authority, - fee_payer: Some(fee_payer), + fee_payer, } + .with_max_top_up(500) .instruction() .expect("Failed to create instruction"); - // Hardcoded expected instruction - // - authority is readonly (fee_payer pays instead) - // - fee_payer is 5th account: writable, signer - // - data: discriminator (3) + amount (8 bytes) = 9 bytes (short format) let expected = Instruction { program_id: LIGHT_TOKEN_PROGRAM_ID, accounts: vec![ - AccountMeta::new(source, false), // source: writable, not signer - AccountMeta::new(destination, false), // destination: writable, not signer - AccountMeta::new_readonly(authority, true), // authority: readonly, signer - AccountMeta::new_readonly(Pubkey::default(), false), // system_program: readonly, not signer - AccountMeta::new(fee_payer, true), // fee_payer: writable, signer + AccountMeta::new(source, false), + AccountMeta::new(destination, false), + AccountMeta::new_readonly(authority, true), + AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new(fee_payer, true), ], data: vec![ 3u8, // Transfer discriminator 100, 0, 0, 0, 0, 0, 0, 0, // amount: 100 as little-endian u64 + 244, 1, // max_top_up: 500 as little-endian u16 ], }; assert_eq!( instruction, expected, - "Transfer instruction with fee_payer should match expected" + "Transfer instruction with max_top_up should match expected" ); } diff --git a/sdk-libs/token-sdk/tests/transfer_type.rs b/sdk-libs/token-sdk/tests/transfer_type.rs index 47e7db1b79..2e1a67f1df 100644 --- a/sdk-libs/token-sdk/tests/transfer_type.rs +++ b/sdk-libs/token-sdk/tests/transfer_type.rs @@ -112,6 +112,7 @@ fn test_transfer_interface_light_to_light_no_spl_interface() { authority, payer, spl_interface: None, // No SPL interface needed + source_owner: LIGHT_TOKEN_PROGRAM_ID, destination_owner: LIGHT_TOKEN_PROGRAM_ID, }; @@ -146,6 +147,7 @@ fn test_transfer_interface_light_to_spl_requires_interface() { authority, payer, spl_interface: None, // Missing required interface + source_owner: LIGHT_TOKEN_PROGRAM_ID, destination_owner: SPL_TOKEN_PROGRAM_ID, }; @@ -175,6 +177,7 @@ fn test_transfer_interface_spl_to_light_requires_interface() { authority, payer, spl_interface: None, // Missing required interface + source_owner: SPL_TOKEN_PROGRAM_ID, destination_owner: LIGHT_TOKEN_PROGRAM_ID, }; @@ -210,6 +213,7 @@ fn test_transfer_interface_light_to_spl_with_interface() { spl_interface_pda, spl_interface_pda_bump: 255, }), + source_owner: LIGHT_TOKEN_PROGRAM_ID, destination_owner: SPL_TOKEN_PROGRAM_ID, }; @@ -239,6 +243,7 @@ fn test_transfer_interface_spl_to_spl_requires_interface() { authority, payer, spl_interface: None, // Missing interface + source_owner: SPL_TOKEN_PROGRAM_ID, destination_owner: SPL_TOKEN_PROGRAM_ID, }; @@ -275,6 +280,7 @@ fn test_transfer_interface_spl_program_mismatch() { spl_interface_pda, spl_interface_pda_bump: 255, }), + source_owner: SPL_TOKEN_PROGRAM_ID, destination_owner: SPL_TOKEN_2022_PROGRAM_ID, }; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/deposit.rs b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/deposit.rs index 2ef54933d5..8d1c5da797 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/deposit.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/deposit.rs @@ -79,7 +79,7 @@ pub fn process_deposit(ctx: Context, lp_token_amount: u64) -> Result<() amount: lp_token_amount, authority: ctx.accounts.authority.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), - fee_payer: None, + fee_payer: ctx.accounts.owner.to_account_info(), } .invoke_signed(&[&[AUTH_SEED.as_bytes(), &[auth_bump]]])?; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs index 4864f83e3b..3dea016afd 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs @@ -254,7 +254,7 @@ pub fn process_initialize_pool<'info>( amount: lp_amount, authority: ctx.accounts.authority.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), - fee_payer: None, + fee_payer: ctx.accounts.creator.to_account_info(), } .invoke_signed(&[&[AUTH_SEED.as_bytes(), &[ctx.bumps.authority]]])?; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/withdraw.rs b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/withdraw.rs index 5416c8abdb..c5199a8c0a 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/withdraw.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/withdraw.rs @@ -76,7 +76,7 @@ pub fn process_withdraw(ctx: Context, lp_token_amount: u64) -> Result< amount: lp_token_amount, authority: ctx.accounts.owner.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), - fee_payer: None, + fee_payer: ctx.accounts.owner.to_account_info(), } .invoke()?; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs index 1282ebb68b..ed4f05aee7 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -385,7 +385,7 @@ pub mod csdk_anchor_full_derived_test { amount: params.vault_mint_amount, authority: ctx.accounts.mint_authority.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), - fee_payer: None, + fee_payer: ctx.accounts.fee_payer.to_account_info(), } .invoke()?; } @@ -397,7 +397,7 @@ pub mod csdk_anchor_full_derived_test { amount: params.user_ata_mint_amount, authority: ctx.accounts.mint_authority.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), - fee_payer: None, + fee_payer: ctx.accounts.fee_payer.to_account_info(), } .invoke()?; } @@ -1601,7 +1601,7 @@ pub mod csdk_anchor_full_derived_test { amount: params.mint_amount, authority: ctx.accounts.mint_authority.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), - fee_payer: None, + fee_payer: ctx.accounts.fee_payer.to_account_info(), } .invoke()?; } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/shared.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/shared.rs index db52561bec..e2f75c07d5 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/shared.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/shared.rs @@ -341,7 +341,7 @@ pub async fn setup_create_mint( destination: ata_pubkeys[*idx], amount: *amount, authority: mint_authority, - fee_payer: None, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); diff --git a/sdk-tests/sdk-light-token-pinocchio/src/approve.rs b/sdk-tests/sdk-light-token-pinocchio/src/approve.rs index 3aacf4d3ed..001b87be00 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/approve.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/approve.rs @@ -36,6 +36,7 @@ pub fn process_approve_invoke( owner: &accounts[2], system_program: &accounts[3], amount: data.amount, + fee_payer: &accounts[2], } .invoke()?; @@ -74,6 +75,7 @@ pub fn process_approve_invoke_signed( owner: &accounts[2], system_program: &accounts[3], amount: data.amount, + fee_payer: &accounts[2], } .invoke_signed(&[signer])?; diff --git a/sdk-tests/sdk-light-token-pinocchio/src/burn.rs b/sdk-tests/sdk-light-token-pinocchio/src/burn.rs index be576994ca..c4d8c6dde8 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/burn.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/burn.rs @@ -33,7 +33,7 @@ pub fn process_burn_invoke(accounts: &[AccountInfo], amount: u64) -> Result<(), amount, authority: &accounts[2], system_program: &accounts[4], - fee_payer: None, + fee_payer: &accounts[2], } .invoke()?; @@ -48,11 +48,12 @@ pub fn process_burn_invoke(accounts: &[AccountInfo], amount: u64) -> Result<(), /// - accounts[2]: PDA authority (owner, program signs) /// - accounts[3]: light_token_program /// - accounts[4]: system_program +/// - accounts[5]: fee_payer (writable, signer) pub fn process_burn_invoke_signed( accounts: &[AccountInfo], amount: u64, ) -> Result<(), ProgramError> { - if accounts.len() < 5 { + if accounts.len() < 6 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -73,7 +74,7 @@ pub fn process_burn_invoke_signed( amount, authority: &accounts[2], system_program: &accounts[4], - fee_payer: None, + fee_payer: &accounts[5], } .invoke_signed(&[signer])?; diff --git a/sdk-tests/sdk-light-token-pinocchio/src/ctoken_mint_to.rs b/sdk-tests/sdk-light-token-pinocchio/src/ctoken_mint_to.rs index 95920489aa..8f53668856 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/ctoken_mint_to.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/ctoken_mint_to.rs @@ -33,7 +33,7 @@ pub fn process_mint_to_invoke(accounts: &[AccountInfo], amount: u64) -> Result<( amount, authority: &accounts[2], system_program: &accounts[3], - fee_payer: None, + fee_payer: &accounts[2], } .invoke()?; @@ -48,11 +48,12 @@ pub fn process_mint_to_invoke(accounts: &[AccountInfo], amount: u64) -> Result<( /// - accounts[2]: PDA authority (mint authority, program signs) /// - accounts[3]: system_program /// - accounts[4]: light_token_program +/// - accounts[5]: fee_payer (writable, signer) pub fn process_mint_to_invoke_signed( accounts: &[AccountInfo], amount: u64, ) -> Result<(), ProgramError> { - if accounts.len() < 5 { + if accounts.len() < 6 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -74,7 +75,7 @@ pub fn process_mint_to_invoke_signed( amount, authority: &accounts[2], system_program: &accounts[3], - fee_payer: None, + fee_payer: &accounts[5], } .invoke_signed(&[signer])?; diff --git a/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs b/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs index a1fb1508f9..c1ddf7ff93 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs @@ -23,6 +23,7 @@ pub fn process_revoke_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramErro token_account: &accounts[0], owner: &accounts[1], system_program: &accounts[2], + fee_payer: &accounts[1], } .invoke()?; @@ -57,6 +58,7 @@ pub fn process_revoke_invoke_signed(accounts: &[AccountInfo]) -> Result<(), Prog token_account: &accounts[0], owner: &accounts[1], system_program: &accounts[2], + fee_payer: &accounts[1], } .invoke_signed(&[signer])?; diff --git a/sdk-tests/sdk-light-token-pinocchio/src/transfer.rs b/sdk-tests/sdk-light-token-pinocchio/src/transfer.rs index 4958e1faa3..bc3aff46c1 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/transfer.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/transfer.rs @@ -40,7 +40,7 @@ pub fn process_transfer_invoke( amount: data.amount, authority: &accounts[2], system_program: &accounts[3], - fee_payer: None, + fee_payer: &accounts[2], } .invoke()?; @@ -59,11 +59,12 @@ pub fn process_transfer_invoke( /// - accounts[1]: destination ctoken account /// - accounts[2]: authority (PDA) /// - accounts[3]: system_program +/// - accounts[4]: fee_payer (writable, signer) pub fn process_transfer_invoke_signed( accounts: &[AccountInfo], data: TransferData, ) -> Result<(), ProgramError> { - if accounts.len() < 4 { + if accounts.len() < 5 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -82,7 +83,7 @@ pub fn process_transfer_invoke_signed( amount: data.amount, authority: &accounts[2], system_program: &accounts[3], - fee_payer: None, + fee_payer: &accounts[4], }; // Invoke with PDA signing - the builder handles instruction creation and invoke_signed CPI diff --git a/sdk-tests/sdk-light-token-pinocchio/src/transfer_checked.rs b/sdk-tests/sdk-light-token-pinocchio/src/transfer_checked.rs index 7bf130bce1..8abfa102da 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/transfer_checked.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/transfer_checked.rs @@ -39,7 +39,7 @@ pub fn process_transfer_checked_invoke( decimals: data.decimals, authority: &accounts[3], system_program: &accounts[4], - fee_payer: None, + fee_payer: &accounts[3], } .invoke()?; @@ -54,11 +54,12 @@ pub fn process_transfer_checked_invoke( /// - accounts[2]: destination ctoken account /// - accounts[3]: authority (PDA) /// - accounts[4]: system_program +/// - accounts[5]: fee_payer (writable, signer) pub fn process_transfer_checked_invoke_signed( accounts: &[AccountInfo], data: TransferCheckedData, ) -> Result<(), ProgramError> { - if accounts.len() < 5 { + if accounts.len() < 6 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -78,7 +79,7 @@ pub fn process_transfer_checked_invoke_signed( decimals: data.decimals, authority: &accounts[3], system_program: &accounts[4], - fee_payer: None, + fee_payer: &accounts[5], }; // Invoke with PDA signing diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/shared/mod.rs b/sdk-tests/sdk-light-token-pinocchio/tests/shared/mod.rs index 8ad9cb03df..252ee7d4c5 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/shared/mod.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/shared/mod.rs @@ -127,7 +127,7 @@ pub async fn setup_create_mint( destination: ata_pubkeys[*idx], amount: *amount, authority: mint_authority, - fee_payer: None, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); @@ -247,7 +247,7 @@ pub async fn setup_create_mint_with_freeze_authority( destination: ata_pubkeys[*idx], amount: *amount, authority: mint_authority, - fee_payer: None, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); @@ -386,7 +386,7 @@ pub async fn setup_create_mint_with_compression_only( destination: ata_pubkeys[*idx], amount: *amount, authority: mint_authority, - fee_payer: None, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_burn.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_burn.rs index ece1d29f34..dd2a1814d3 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/test_burn.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_burn.rs @@ -128,6 +128,7 @@ async fn test_burn_invoke_signed() { AccountMeta::new(pda_owner, false), // PDA authority (writable, program signs) AccountMeta::new_readonly(light_token_program, false), // light_token_program AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(payer.pubkey(), true), // fee_payer (PDA authority != tx fee payer) ], data: instruction_data, }; diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs index 7c29d030b1..504e80f196 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs @@ -234,9 +234,10 @@ async fn test_ctoken_mint_to_invoke_signed() { accounts: vec![ AccountMeta::new(mint_pda, false), // mint AccountMeta::new(ata, false), // destination - AccountMeta::new(pda_mint_authority, false), // PDA authority (program signs, writable for top-up) + AccountMeta::new_readonly(pda_mint_authority, false), // PDA authority (program signs, readonly) AccountMeta::new_readonly(system_program, false), // system_program AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new(payer.pubkey(), true), // fee_payer (PDA authority != tx fee payer) ], data: instruction_data, }; diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs index 4f8bc73872..a60147079d 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs @@ -116,6 +116,7 @@ async fn test_ctoken_transfer_invoke_signed() { AccountMeta::new(pda_owner, false), // PDA authority (writable, program signs) AccountMeta::new_readonly(Pubkey::default(), false), // system_program AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + AccountMeta::new(payer.pubkey(), true), // fee_payer (PDA authority != tx fee payer) ], data: instruction_data, }; diff --git a/sdk-tests/sdk-light-token-test/src/approve.rs b/sdk-tests/sdk-light-token-test/src/approve.rs index 0dda096b88..2f5a97768e 100644 --- a/sdk-tests/sdk-light-token-test/src/approve.rs +++ b/sdk-tests/sdk-light-token-test/src/approve.rs @@ -32,6 +32,7 @@ pub fn process_approve_invoke( owner: accounts[2].clone(), system_program: accounts[3].clone(), amount: data.amount, + fee_payer: accounts[2].clone(), } .invoke()?; @@ -69,6 +70,7 @@ pub fn process_approve_invoke_signed( owner: accounts[2].clone(), system_program: accounts[3].clone(), amount: data.amount, + fee_payer: accounts[2].clone(), } .invoke_signed(&[signer_seeds])?; diff --git a/sdk-tests/sdk-light-token-test/src/burn.rs b/sdk-tests/sdk-light-token-test/src/burn.rs index a6dbc0a1ca..f2a2a9e4a4 100644 --- a/sdk-tests/sdk-light-token-test/src/burn.rs +++ b/sdk-tests/sdk-light-token-test/src/burn.rs @@ -29,7 +29,7 @@ pub fn process_burn_invoke(accounts: &[AccountInfo], amount: u64) -> Result<(), amount, authority: accounts[2].clone(), system_program: accounts[4].clone(), - fee_payer: None, + fee_payer: accounts[2].clone(), } .invoke()?; @@ -44,11 +44,12 @@ pub fn process_burn_invoke(accounts: &[AccountInfo], amount: u64) -> Result<(), /// - accounts[2]: PDA authority (owner, program signs) /// - accounts[3]: light_token_program /// - accounts[4]: system_program +/// - accounts[5]: fee_payer (writable, signer) pub fn process_burn_invoke_signed( accounts: &[AccountInfo], amount: u64, ) -> Result<(), ProgramError> { - if accounts.len() < 5 { + if accounts.len() < 6 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -67,7 +68,7 @@ pub fn process_burn_invoke_signed( amount, authority: accounts[2].clone(), system_program: accounts[4].clone(), - fee_payer: None, + fee_payer: accounts[5].clone(), } .invoke_signed(&[signer_seeds])?; diff --git a/sdk-tests/sdk-light-token-test/src/ctoken_mint_to.rs b/sdk-tests/sdk-light-token-test/src/ctoken_mint_to.rs index 8a70311472..fc8a72ee33 100644 --- a/sdk-tests/sdk-light-token-test/src/ctoken_mint_to.rs +++ b/sdk-tests/sdk-light-token-test/src/ctoken_mint_to.rs @@ -29,7 +29,7 @@ pub fn process_mint_to_invoke(accounts: &[AccountInfo], amount: u64) -> Result<( amount, authority: accounts[2].clone(), system_program: accounts[3].clone(), - fee_payer: None, + fee_payer: accounts[2].clone(), } .invoke()?; @@ -44,11 +44,12 @@ pub fn process_mint_to_invoke(accounts: &[AccountInfo], amount: u64) -> Result<( /// - accounts[2]: PDA authority (mint authority, program signs) /// - accounts[3]: system_program /// - accounts[4]: light_token_program +/// - accounts[5]: fee_payer (writable, signer) pub fn process_mint_to_invoke_signed( accounts: &[AccountInfo], amount: u64, ) -> Result<(), ProgramError> { - if accounts.len() < 5 { + if accounts.len() < 6 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -67,7 +68,7 @@ pub fn process_mint_to_invoke_signed( amount, authority: accounts[2].clone(), system_program: accounts[3].clone(), - fee_payer: None, + fee_payer: accounts[5].clone(), } .invoke_signed(&[signer_seeds])?; diff --git a/sdk-tests/sdk-light-token-test/src/revoke.rs b/sdk-tests/sdk-light-token-test/src/revoke.rs index ff55fbecce..47fae3d41a 100644 --- a/sdk-tests/sdk-light-token-test/src/revoke.rs +++ b/sdk-tests/sdk-light-token-test/src/revoke.rs @@ -19,6 +19,7 @@ pub fn process_revoke_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramErro token_account: accounts[0].clone(), owner: accounts[1].clone(), system_program: accounts[2].clone(), + fee_payer: accounts[1].clone(), } .invoke()?; @@ -50,6 +51,7 @@ pub fn process_revoke_invoke_signed(accounts: &[AccountInfo]) -> Result<(), Prog token_account: accounts[0].clone(), owner: accounts[1].clone(), system_program: accounts[2].clone(), + fee_payer: accounts[1].clone(), } .invoke_signed(&[signer_seeds])?; diff --git a/sdk-tests/sdk-light-token-test/src/transfer.rs b/sdk-tests/sdk-light-token-test/src/transfer.rs index c9c80c2fa8..e38d086893 100644 --- a/sdk-tests/sdk-light-token-test/src/transfer.rs +++ b/sdk-tests/sdk-light-token-test/src/transfer.rs @@ -36,7 +36,7 @@ pub fn process_transfer_invoke( amount: data.amount, authority: accounts[2].clone(), system_program: accounts[3].clone(), - fee_payer: None, + fee_payer: accounts[2].clone(), } .invoke()?; @@ -55,11 +55,12 @@ pub fn process_transfer_invoke( /// - accounts[1]: destination ctoken account /// - accounts[2]: authority (PDA) /// - accounts[3]: system_program +/// - accounts[4]: fee_payer (writable, signer) pub fn process_transfer_invoke_signed( accounts: &[AccountInfo], data: TransferData, ) -> Result<(), ProgramError> { - if accounts.len() < 4 { + if accounts.len() < 5 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -78,7 +79,7 @@ pub fn process_transfer_invoke_signed( amount: data.amount, authority: accounts[2].clone(), system_program: accounts[3].clone(), - fee_payer: None, + fee_payer: accounts[4].clone(), }; // Invoke with PDA signing - the builder handles instruction creation and invoke_signed CPI diff --git a/sdk-tests/sdk-light-token-test/src/transfer_checked.rs b/sdk-tests/sdk-light-token-test/src/transfer_checked.rs index 89382299e1..043c6580e0 100644 --- a/sdk-tests/sdk-light-token-test/src/transfer_checked.rs +++ b/sdk-tests/sdk-light-token-test/src/transfer_checked.rs @@ -35,7 +35,7 @@ pub fn process_transfer_checked_invoke( decimals: data.decimals, authority: accounts[3].clone(), system_program: accounts[4].clone(), - fee_payer: None, + fee_payer: accounts[3].clone(), } .invoke()?; @@ -50,11 +50,12 @@ pub fn process_transfer_checked_invoke( /// - accounts[2]: destination ctoken account /// - accounts[3]: authority (PDA) /// - accounts[4]: system_program +/// - accounts[5]: fee_payer (writable, signer) pub fn process_transfer_checked_invoke_signed( accounts: &[AccountInfo], data: TransferCheckedData, ) -> Result<(), ProgramError> { - if accounts.len() < 5 { + if accounts.len() < 6 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -74,7 +75,7 @@ pub fn process_transfer_checked_invoke_signed( decimals: data.decimals, authority: accounts[3].clone(), system_program: accounts[4].clone(), - fee_payer: None, + fee_payer: accounts[5].clone(), }; // Invoke with PDA signing diff --git a/sdk-tests/sdk-light-token-test/tests/scenario_light_mint.rs b/sdk-tests/sdk-light-token-test/tests/scenario_light_mint.rs index dfd61c063d..976e6fca7d 100644 --- a/sdk-tests/sdk-light-token-test/tests/scenario_light_mint.rs +++ b/sdk-tests/sdk-light-token-test/tests/scenario_light_mint.rs @@ -93,7 +93,7 @@ async fn test_mint_to_ctoken_scenario() { destination: ctoken_ata2, amount: transfer_amount, authority: owner1.pubkey(), - fee_payer: None, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); diff --git a/sdk-tests/sdk-light-token-test/tests/scenario_light_mint_compression_only.rs b/sdk-tests/sdk-light-token-test/tests/scenario_light_mint_compression_only.rs index 2d0b444712..aa7d3e1a27 100644 --- a/sdk-tests/sdk-light-token-test/tests/scenario_light_mint_compression_only.rs +++ b/sdk-tests/sdk-light-token-test/tests/scenario_light_mint_compression_only.rs @@ -98,7 +98,7 @@ async fn test_mint_to_ctoken_scenario_compression_only() { destination: ctoken_ata2, amount: transfer_amount, authority: owner1.pubkey(), - fee_payer: None, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); diff --git a/sdk-tests/sdk-light-token-test/tests/shared.rs b/sdk-tests/sdk-light-token-test/tests/shared.rs index 42faa0aac3..d425c3d229 100644 --- a/sdk-tests/sdk-light-token-test/tests/shared.rs +++ b/sdk-tests/sdk-light-token-test/tests/shared.rs @@ -123,7 +123,7 @@ pub async fn setup_create_mint( destination: ata_pubkeys[*idx], amount: *amount, authority: mint_authority, - fee_payer: None, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); @@ -243,7 +243,7 @@ pub async fn setup_create_mint_with_freeze_authority( destination: ata_pubkeys[*idx], amount: *amount, authority: mint_authority, - fee_payer: None, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); @@ -382,7 +382,7 @@ pub async fn setup_create_mint_with_compression_only( destination: ata_pubkeys[*idx], amount: *amount, authority: mint_authority, - fee_payer: None, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); diff --git a/sdk-tests/sdk-light-token-test/tests/test_burn.rs b/sdk-tests/sdk-light-token-test/tests/test_burn.rs index 99428eb778..9fc69ba10c 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_burn.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_burn.rs @@ -119,9 +119,10 @@ async fn test_burn_invoke_signed() { accounts: vec![ AccountMeta::new(ata, false), // source AccountMeta::new(mint_pda, false), // mint - AccountMeta::new(pda_owner, false), // PDA authority (writable, pays for top-ups) + AccountMeta::new_readonly(pda_owner, false), // PDA authority (program signs) AccountMeta::new_readonly(light_token_program, false), // light_token_program - AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(payer.pubkey(), true), // fee_payer ], data: instruction_data, }; diff --git a/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs b/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs index 96e2de66b8..044a38388a 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs @@ -169,18 +169,18 @@ async fn test_ctoken_mint_to_invoke_signed() { AccountMeta::new_readonly(compressed_token_program_id, false), // [0] AccountMeta::new_readonly(default_pubkeys.light_system_program, false), // [1] AccountMeta::new_readonly(mint_signer_pda, false), // [2] mint_signer PDA - AccountMeta::new(pda_mint_authority, false), // [3] authority PDA (writable, pays for top-ups) + AccountMeta::new_readonly(pda_mint_authority, false), // [3] authority PDA AccountMeta::new_readonly(compressible_config, false), // [4] compressible_config - AccountMeta::new(mint_pda, false), // [5] mint - AccountMeta::new(rent_sponsor, false), // [6] rent_sponsor - AccountMeta::new(payer.pubkey(), true), // [7] fee_payer (signer) + AccountMeta::new(mint_pda, false), // [5] mint + AccountMeta::new(rent_sponsor, false), // [6] rent_sponsor + AccountMeta::new(payer.pubkey(), true), // [7] fee_payer (signer) AccountMeta::new_readonly(default_pubkeys.cpi_authority_pda, false), // [8] AccountMeta::new_readonly(default_pubkeys.registered_program_pda, false), // [9] AccountMeta::new_readonly(default_pubkeys.account_compression_authority, false), // [10] AccountMeta::new_readonly(default_pubkeys.account_compression_program, false), // [11] AccountMeta::new_readonly(default_pubkeys.system_program, false), // [12] - AccountMeta::new(output_queue, false), // [13] - AccountMeta::new(address_tree.tree, false), // [14] + AccountMeta::new(output_queue, false), // [13] + AccountMeta::new(address_tree.tree, false), // [14] ]; let create_mint_ix = Instruction { @@ -228,9 +228,10 @@ async fn test_ctoken_mint_to_invoke_signed() { accounts: vec![ AccountMeta::new(mint_pda, false), // mint AccountMeta::new(ata, false), // destination - AccountMeta::new(pda_mint_authority, false), // PDA authority (program signs, writable for top-up) - AccountMeta::new_readonly(system_program, false), // system_program + AccountMeta::new_readonly(pda_mint_authority, false), // PDA authority (program signs) + AccountMeta::new_readonly(system_program, false), // system_program AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new(payer.pubkey(), true), // fee_payer ], data: instruction_data, }; diff --git a/sdk-tests/sdk-light-token-test/tests/test_transfer.rs b/sdk-tests/sdk-light-token-test/tests/test_transfer.rs index 98038f1566..b7ac69e2e9 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_transfer.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_transfer.rs @@ -107,8 +107,9 @@ async fn test_ctoken_transfer_invoke_signed() { accounts: vec![ AccountMeta::new(source_ata, false), AccountMeta::new(dest_ata, false), - AccountMeta::new(pda_owner, false), // PDA authority (writable, pays for top-ups) + AccountMeta::new_readonly(pda_owner, false), // PDA authority, not signer AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(payer.pubkey(), true), // fee_payer AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), ], data: instruction_data, From 1789ebe44867c190711de4f5c256e40a44ed85b0 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Fri, 20 Feb 2026 18:45:04 +0000 Subject: [PATCH 14/26] before ci --- .../src/instruction/approve.rs | 6 ++--- .../token-pinocchio/src/instruction/revoke.rs | 12 ++++++--- sdk-libs/token-sdk/src/instruction/approve.rs | 13 +++++----- sdk-libs/token-sdk/src/instruction/revoke.rs | 25 ++++++++++++------- .../src/transfer_checked.rs | 4 +-- 5 files changed, 35 insertions(+), 25 deletions(-) diff --git a/sdk-libs/token-pinocchio/src/instruction/approve.rs b/sdk-libs/token-pinocchio/src/instruction/approve.rs index e50d7831ad..c7eb0a6ba5 100644 --- a/sdk-libs/token-pinocchio/src/instruction/approve.rs +++ b/sdk-libs/token-pinocchio/src/instruction/approve.rs @@ -33,8 +33,7 @@ pub struct ApproveCpi<'info> { pub owner: &'info AccountInfo, pub system_program: &'info AccountInfo, pub amount: u64, - // TODO: fee_payer will be sent as a separate account when on-chain supports it. - /// Fee payer for rent top-ups. Not yet sent to on-chain (owner pays instead). + /// Fee payer for compressible rent top-ups (writable signer) pub fee_payer: &'info AccountInfo, } @@ -51,12 +50,12 @@ impl<'info> ApproveCpi<'info> { let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); - // Owner is writable (on-chain requires it — no fee_payer support yet) let account_metas = [ AccountMeta::writable(self.token_account.key()), AccountMeta::readonly(self.delegate.key()), AccountMeta::writable_signer(self.owner.key()), AccountMeta::readonly(self.system_program.key()), + AccountMeta::writable_signer(self.fee_payer.key()), ]; let instruction = Instruction { @@ -70,6 +69,7 @@ impl<'info> ApproveCpi<'info> { self.delegate, self.owner, self.system_program, + self.fee_payer, ]; if signers.is_empty() { diff --git a/sdk-libs/token-pinocchio/src/instruction/revoke.rs b/sdk-libs/token-pinocchio/src/instruction/revoke.rs index 31fc305148..0f8b490747 100644 --- a/sdk-libs/token-pinocchio/src/instruction/revoke.rs +++ b/sdk-libs/token-pinocchio/src/instruction/revoke.rs @@ -29,8 +29,7 @@ pub struct RevokeCpi<'info> { pub token_account: &'info AccountInfo, pub owner: &'info AccountInfo, pub system_program: &'info AccountInfo, - // TODO: fee_payer will be sent as a separate account when on-chain supports it. - /// Fee payer for rent top-ups. Not yet sent to on-chain (owner pays instead). + /// Fee payer for compressible rent top-ups (writable signer) pub fee_payer: &'info AccountInfo, } @@ -45,11 +44,11 @@ impl<'info> RevokeCpi<'info> { let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); - // Owner is writable (on-chain requires it — no fee_payer support yet) let account_metas = [ AccountMeta::writable(self.token_account.key()), AccountMeta::writable_signer(self.owner.key()), AccountMeta::readonly(self.system_program.key()), + AccountMeta::writable_signer(self.fee_payer.key()), ]; let instruction = Instruction { @@ -58,7 +57,12 @@ impl<'info> RevokeCpi<'info> { data: &data, }; - let account_infos = [self.token_account, self.owner, self.system_program]; + let account_infos = [ + self.token_account, + self.owner, + self.system_program, + self.fee_payer, + ]; if signers.is_empty() { slice_invoke(&instruction, &account_infos) diff --git a/sdk-libs/token-sdk/src/instruction/approve.rs b/sdk-libs/token-sdk/src/instruction/approve.rs index 059138bed4..52150ebb3b 100644 --- a/sdk-libs/token-sdk/src/instruction/approve.rs +++ b/sdk-libs/token-sdk/src/instruction/approve.rs @@ -27,13 +27,11 @@ pub struct Approve { pub token_account: Pubkey, /// Delegate to approve pub delegate: Pubkey, - /// Owner of the Light Token account (signer, writable — on-chain requires owner to pay) + /// Owner of the Light Token account (writable signer) pub owner: Pubkey, /// Amount of tokens to delegate pub amount: u64, - // TODO: fee_payer will be sent as a separate account when on-chain supports it. - // Currently owner pays for top-ups directly. - /// Fee payer for rent top-ups. Not yet sent to on-chain (owner pays instead). + /// Fee payer for compressible rent top-ups (writable signer) pub fee_payer: Pubkey, } @@ -63,8 +61,7 @@ pub struct ApproveCpi<'info> { pub owner: AccountInfo<'info>, pub system_program: AccountInfo<'info>, pub amount: u64, - // TODO: fee_payer will be sent as a separate account when on-chain supports it. - /// Fee payer for rent top-ups. Not yet sent to on-chain (owner pays instead). + /// Fee payer for compressible rent top-ups (writable signer) pub fee_payer: AccountInfo<'info>, } @@ -80,6 +77,7 @@ impl<'info> ApproveCpi<'info> { self.delegate, self.owner, self.system_program, + self.fee_payer, ]; invoke(&instruction, &account_infos) } @@ -91,6 +89,7 @@ impl<'info> ApproveCpi<'info> { self.delegate, self.owner, self.system_program, + self.fee_payer, ]; invoke_signed(&instruction, &account_infos, signer_seeds) } @@ -115,12 +114,12 @@ impl Approve { Ok(Instruction { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), - // Owner is writable (on-chain requires it — no fee_payer support yet) accounts: vec![ AccountMeta::new(self.token_account, false), AccountMeta::new_readonly(self.delegate, false), AccountMeta::new(self.owner, true), AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new(self.fee_payer, true), ], data, }) diff --git a/sdk-libs/token-sdk/src/instruction/revoke.rs b/sdk-libs/token-sdk/src/instruction/revoke.rs index 32aa497555..c91197b2e9 100644 --- a/sdk-libs/token-sdk/src/instruction/revoke.rs +++ b/sdk-libs/token-sdk/src/instruction/revoke.rs @@ -22,11 +22,9 @@ use solana_pubkey::Pubkey; pub struct Revoke { /// Light Token account to revoke delegation for pub token_account: Pubkey, - /// Owner of the Light Token account (signer, writable — on-chain requires owner to pay) + /// Owner of the Light Token account (writable signer) pub owner: Pubkey, - // TODO: fee_payer will be sent as a separate account when on-chain supports it. - // Currently owner pays for top-ups directly. - /// Fee payer for rent top-ups. Not yet sent to on-chain (owner pays instead). + /// Fee payer for compressible rent top-ups (writable signer) pub fee_payer: Pubkey, } @@ -51,8 +49,7 @@ pub struct RevokeCpi<'info> { pub token_account: AccountInfo<'info>, pub owner: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - // TODO: fee_payer will be sent as a separate account when on-chain supports it. - /// Fee payer for rent top-ups. Not yet sent to on-chain (owner pays instead). + /// Fee payer for compressible rent top-ups (writable signer) pub fee_payer: AccountInfo<'info>, } @@ -63,13 +60,23 @@ impl<'info> RevokeCpi<'info> { pub fn invoke(self) -> Result<(), ProgramError> { let instruction = Revoke::from(&self).instruction()?; - let account_infos = [self.token_account, self.owner, self.system_program]; + let account_infos = [ + self.token_account, + self.owner, + self.system_program, + self.fee_payer, + ]; invoke(&instruction, &account_infos) } pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { let instruction = Revoke::from(&self).instruction()?; - let account_infos = [self.token_account, self.owner, self.system_program]; + let account_infos = [ + self.token_account, + self.owner, + self.system_program, + self.fee_payer, + ]; invoke_signed(&instruction, &account_infos, signer_seeds) } } @@ -88,11 +95,11 @@ impl Revoke { pub fn instruction(self) -> Result { Ok(Instruction { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), - // Owner is writable (on-chain requires it — no fee_payer support yet) accounts: vec![ AccountMeta::new(self.token_account, false), AccountMeta::new(self.owner, true), AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new(self.fee_payer, true), ], data: vec![5u8], // CTokenRevoke discriminator }) diff --git a/sdk-tests/sdk-light-token-pinocchio/src/transfer_checked.rs b/sdk-tests/sdk-light-token-pinocchio/src/transfer_checked.rs index 8abfa102da..d57842b034 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/transfer_checked.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/transfer_checked.rs @@ -39,7 +39,7 @@ pub fn process_transfer_checked_invoke( decimals: data.decimals, authority: &accounts[3], system_program: &accounts[4], - fee_payer: &accounts[3], + fee_payer: Some(&accounts[3]), } .invoke()?; @@ -79,7 +79,7 @@ pub fn process_transfer_checked_invoke_signed( decimals: data.decimals, authority: &accounts[3], system_program: &accounts[4], - fee_payer: &accounts[5], + fee_payer: Some(&accounts[5]), }; // Invoke with PDA signing From 2fff6778786e078cbe1463004ddc59ccab1b6482 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Fri, 20 Feb 2026 18:55:00 +0000 Subject: [PATCH 15/26] style: fix formatting in sdk test files --- .../sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs | 2 +- sdk-tests/sdk-light-token-test/tests/test_transfer.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs index 504e80f196..d6926e8d34 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs @@ -235,7 +235,7 @@ async fn test_ctoken_mint_to_invoke_signed() { AccountMeta::new(mint_pda, false), // mint AccountMeta::new(ata, false), // destination AccountMeta::new_readonly(pda_mint_authority, false), // PDA authority (program signs, readonly) - AccountMeta::new_readonly(system_program, false), // system_program + AccountMeta::new_readonly(system_program, false), // system_program AccountMeta::new_readonly(light_token_program, false), // light_token_program AccountMeta::new(payer.pubkey(), true), // fee_payer (PDA authority != tx fee payer) ], diff --git a/sdk-tests/sdk-light-token-test/tests/test_transfer.rs b/sdk-tests/sdk-light-token-test/tests/test_transfer.rs index b7ac69e2e9..20394128a1 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_transfer.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_transfer.rs @@ -109,7 +109,7 @@ async fn test_ctoken_transfer_invoke_signed() { AccountMeta::new(dest_ata, false), AccountMeta::new_readonly(pda_owner, false), // PDA authority, not signer AccountMeta::new_readonly(Pubkey::default(), false), // system_program - AccountMeta::new(payer.pubkey(), true), // fee_payer + AccountMeta::new(payer.pubkey(), true), // fee_payer AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), ], data: instruction_data, From 16a7f3b0c11a51c8df38b57e21e7f239a45bd966 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Fri, 20 Feb 2026 19:30:22 +0000 Subject: [PATCH 16/26] fix(sdk-test): swap fee_payer and program accounts in transfer test fee_payer must be at index 4 to match TransferCpi handler field order. LIGHT_TOKEN_PROGRAM_ID at index 5 is still resolved by the CPI runtime. --- sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs index a60147079d..3b40ed07a3 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs @@ -115,8 +115,8 @@ async fn test_ctoken_transfer_invoke_signed() { AccountMeta::new(dest_ata, false), AccountMeta::new(pda_owner, false), // PDA authority (writable, program signs) AccountMeta::new_readonly(Pubkey::default(), false), // system_program - AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), AccountMeta::new(payer.pubkey(), true), // fee_payer (PDA authority != tx fee payer) + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), ], data: instruction_data, }; From ad155317e0046c990ed92b42401b1836a7d33e42 Mon Sep 17 00:00:00 2001 From: ananas Date: Fri, 20 Feb 2026 20:35:35 +0000 Subject: [PATCH 17/26] fix(token-sdk): make owner/authority readonly in approve, revoke, close; align transfer_checked - token-sdk approve/revoke: owner AccountMeta new -> new_readonly (readonly signer) - token-pinocchio approve/revoke/close: owner writable_signer -> readonly_signer - token-pinocchio transfer_checked: remove optional fee_payer, make mandatory; authority always readonly_signer; use Pubkey::default() sentinel for system_program in account_metas - token-client revoke: add owner-mismatch guard in execute_with_owner matching approve --- sdk-libs/token-client/src/actions/revoke.rs | 14 +++ .../src/instruction/approve.rs | 2 +- .../token-pinocchio/src/instruction/close.rs | 2 +- .../token-pinocchio/src/instruction/revoke.rs | 2 +- .../src/instruction/transfer_checked.rs | 101 ++++++------------ sdk-libs/token-sdk/src/instruction/approve.rs | 4 +- sdk-libs/token-sdk/src/instruction/revoke.rs | 4 +- .../src/transfer_checked.rs | 7 +- 8 files changed, 55 insertions(+), 81 deletions(-) diff --git a/sdk-libs/token-client/src/actions/revoke.rs b/sdk-libs/token-client/src/actions/revoke.rs index 3852d4841d..a82d0dc5c1 100644 --- a/sdk-libs/token-client/src/actions/revoke.rs +++ b/sdk-libs/token-client/src/actions/revoke.rs @@ -87,12 +87,26 @@ impl Revoke { /// /// # Returns /// `Result` - The transaction signature + /// + /// # Errors + /// Returns an error if `self.owner` is `Some` and does not equal `owner.pubkey()`. pub async fn execute_with_owner( self, rpc: &mut R, payer: &Keypair, owner: &Keypair, ) -> Result { + // Guard: if self.owner is set, it must match the provided owner keypair + if let Some(expected_owner) = self.owner { + if expected_owner != owner.pubkey() { + return Err(RpcError::CustomError(format!( + "owner mismatch: self.owner ({}) does not match owner.pubkey() ({})", + expected_owner, + owner.pubkey() + ))); + } + } + let ix = RevokeInstruction { token_account: self.token_account, owner: owner.pubkey(), diff --git a/sdk-libs/token-pinocchio/src/instruction/approve.rs b/sdk-libs/token-pinocchio/src/instruction/approve.rs index c7eb0a6ba5..430ba8cea1 100644 --- a/sdk-libs/token-pinocchio/src/instruction/approve.rs +++ b/sdk-libs/token-pinocchio/src/instruction/approve.rs @@ -53,7 +53,7 @@ impl<'info> ApproveCpi<'info> { let account_metas = [ AccountMeta::writable(self.token_account.key()), AccountMeta::readonly(self.delegate.key()), - AccountMeta::writable_signer(self.owner.key()), + AccountMeta::readonly_signer(self.owner.key()), AccountMeta::readonly(self.system_program.key()), AccountMeta::writable_signer(self.fee_payer.key()), ]; diff --git a/sdk-libs/token-pinocchio/src/instruction/close.rs b/sdk-libs/token-pinocchio/src/instruction/close.rs index 66922f9d22..80733cdb66 100644 --- a/sdk-libs/token-pinocchio/src/instruction/close.rs +++ b/sdk-libs/token-pinocchio/src/instruction/close.rs @@ -51,7 +51,7 @@ impl<'info> CloseAccountCpi<'info> { let account_metas = [ AccountMeta::writable(self.account.key()), AccountMeta::writable(self.destination.key()), - AccountMeta::writable_signer(self.owner.key()), + AccountMeta::readonly_signer(self.owner.key()), AccountMeta::writable(self.rent_sponsor.key()), ]; diff --git a/sdk-libs/token-pinocchio/src/instruction/revoke.rs b/sdk-libs/token-pinocchio/src/instruction/revoke.rs index 0f8b490747..6c53784e7a 100644 --- a/sdk-libs/token-pinocchio/src/instruction/revoke.rs +++ b/sdk-libs/token-pinocchio/src/instruction/revoke.rs @@ -46,7 +46,7 @@ impl<'info> RevokeCpi<'info> { let account_metas = [ AccountMeta::writable(self.token_account.key()), - AccountMeta::writable_signer(self.owner.key()), + AccountMeta::readonly_signer(self.owner.key()), AccountMeta::readonly(self.system_program.key()), AccountMeta::writable_signer(self.fee_payer.key()), ]; diff --git a/sdk-libs/token-pinocchio/src/instruction/transfer_checked.rs b/sdk-libs/token-pinocchio/src/instruction/transfer_checked.rs index f3b7e8f229..aceebe5a93 100644 --- a/sdk-libs/token-pinocchio/src/instruction/transfer_checked.rs +++ b/sdk-libs/token-pinocchio/src/instruction/transfer_checked.rs @@ -10,6 +10,8 @@ use pinocchio::{ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; +const SYSTEM_PROGRAM_ID: Pubkey = [0u8; 32]; + /// Transfer ctoken checked via CPI. /// /// # Example @@ -25,7 +27,7 @@ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; /// decimals: 9, /// authority: &ctx.accounts.authority, /// system_program: &ctx.accounts.system_program, -/// fee_payer: None, +/// fee_payer: &ctx.accounts.fee_payer, /// } /// .invoke()?; /// ``` @@ -37,8 +39,8 @@ pub struct TransferCheckedCpi<'info> { pub decimals: u8, pub authority: &'info AccountInfo, pub system_program: &'info AccountInfo, - /// Optional fee payer for rent top-ups. If not provided, authority pays. - pub fee_payer: Option<&'info AccountInfo>, + /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + pub fee_payer: &'info AccountInfo, } impl<'info> TransferCheckedCpi<'info> { @@ -47,84 +49,41 @@ impl<'info> TransferCheckedCpi<'info> { } pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { - // Build instruction data: discriminator(1) + amount(8) + decimals(1) + optional max_top_up(2) - let mut data = [0u8; 12]; + let mut data = [0u8; 10]; // discriminator(1) + amount(8) + decimals(1) data[0] = 12u8; // TransferChecked discriminator data[1..9].copy_from_slice(&self.amount.to_le_bytes()); data[9] = self.decimals; - let data_len = 10; - - // Authority is writable only when no fee_payer - let authority_writable = self.fee_payer.is_none(); let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); - if let Some(fee_payer) = self.fee_payer { - let account_metas = [ - AccountMeta::writable(self.source.key()), - AccountMeta::readonly(self.mint.key()), - AccountMeta::writable(self.destination.key()), - if authority_writable { - AccountMeta::writable_signer(self.authority.key()) - } else { - AccountMeta::readonly_signer(self.authority.key()) - }, - AccountMeta::readonly(self.system_program.key()), - AccountMeta::writable_signer(fee_payer.key()), - ]; + let account_metas = [ + AccountMeta::writable(self.source.key()), + AccountMeta::readonly(self.mint.key()), + AccountMeta::writable(self.destination.key()), + AccountMeta::readonly_signer(self.authority.key()), + AccountMeta::readonly(&SYSTEM_PROGRAM_ID), + AccountMeta::writable_signer(self.fee_payer.key()), + ]; - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data[..data_len], - }; + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data, + }; - let account_infos = [ - self.source, - self.mint, - self.destination, - self.authority, - self.system_program, - fee_payer, - ]; + let account_infos = [ + self.source, + self.mint, + self.destination, + self.authority, + self.system_program, + self.fee_payer, + ]; - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) - } else { - slice_invoke_signed(&instruction, &account_infos, signers) - } + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) } else { - let account_metas = [ - AccountMeta::writable(self.source.key()), - AccountMeta::readonly(self.mint.key()), - AccountMeta::writable(self.destination.key()), - if authority_writable { - AccountMeta::writable_signer(self.authority.key()) - } else { - AccountMeta::readonly_signer(self.authority.key()) - }, - AccountMeta::readonly(self.system_program.key()), - ]; - - let instruction = Instruction { - program_id: &program_id, - accounts: &account_metas, - data: &data[..data_len], - }; - - let account_infos = [ - self.source, - self.mint, - self.destination, - self.authority, - self.system_program, - ]; - - if signers.is_empty() { - slice_invoke(&instruction, &account_infos) - } else { - slice_invoke_signed(&instruction, &account_infos, signers) - } + slice_invoke_signed(&instruction, &account_infos, signers) } } } diff --git a/sdk-libs/token-sdk/src/instruction/approve.rs b/sdk-libs/token-sdk/src/instruction/approve.rs index 52150ebb3b..a7de2d4dae 100644 --- a/sdk-libs/token-sdk/src/instruction/approve.rs +++ b/sdk-libs/token-sdk/src/instruction/approve.rs @@ -27,7 +27,7 @@ pub struct Approve { pub token_account: Pubkey, /// Delegate to approve pub delegate: Pubkey, - /// Owner of the Light Token account (writable signer) + /// Owner of the Light Token account (readonly signer) pub owner: Pubkey, /// Amount of tokens to delegate pub amount: u64, @@ -117,7 +117,7 @@ impl Approve { accounts: vec![ AccountMeta::new(self.token_account, false), AccountMeta::new_readonly(self.delegate, false), - AccountMeta::new(self.owner, true), + AccountMeta::new_readonly(self.owner, true), AccountMeta::new_readonly(Pubkey::default(), false), AccountMeta::new(self.fee_payer, true), ], diff --git a/sdk-libs/token-sdk/src/instruction/revoke.rs b/sdk-libs/token-sdk/src/instruction/revoke.rs index c91197b2e9..49df604bed 100644 --- a/sdk-libs/token-sdk/src/instruction/revoke.rs +++ b/sdk-libs/token-sdk/src/instruction/revoke.rs @@ -22,7 +22,7 @@ use solana_pubkey::Pubkey; pub struct Revoke { /// Light Token account to revoke delegation for pub token_account: Pubkey, - /// Owner of the Light Token account (writable signer) + /// Owner of the Light Token account (readonly signer) pub owner: Pubkey, /// Fee payer for compressible rent top-ups (writable signer) pub fee_payer: Pubkey, @@ -97,7 +97,7 @@ impl Revoke { program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), accounts: vec![ AccountMeta::new(self.token_account, false), - AccountMeta::new(self.owner, true), + AccountMeta::new_readonly(self.owner, true), AccountMeta::new_readonly(Pubkey::default(), false), AccountMeta::new(self.fee_payer, true), ], diff --git a/sdk-tests/sdk-light-token-pinocchio/src/transfer_checked.rs b/sdk-tests/sdk-light-token-pinocchio/src/transfer_checked.rs index d57842b034..0bbff0c844 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/transfer_checked.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/transfer_checked.rs @@ -23,11 +23,12 @@ pub struct TransferCheckedData { /// - accounts[2]: destination ctoken account /// - accounts[3]: authority (signer) /// - accounts[4]: system_program +/// - accounts[5]: fee_payer (writable, signer) pub fn process_transfer_checked_invoke( accounts: &[AccountInfo], data: TransferCheckedData, ) -> Result<(), ProgramError> { - if accounts.len() < 5 { + if accounts.len() < 6 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -39,7 +40,7 @@ pub fn process_transfer_checked_invoke( decimals: data.decimals, authority: &accounts[3], system_program: &accounts[4], - fee_payer: Some(&accounts[3]), + fee_payer: &accounts[5], } .invoke()?; @@ -79,7 +80,7 @@ pub fn process_transfer_checked_invoke_signed( decimals: data.decimals, authority: &accounts[3], system_program: &accounts[4], - fee_payer: Some(&accounts[5]), + fee_payer: &accounts[5], }; // Invoke with PDA signing From 050cf75300d8c3152dba6cb551ee48f82bc350da Mon Sep 17 00:00:00 2001 From: ananas Date: Fri, 20 Feb 2026 21:03:00 +0000 Subject: [PATCH 18/26] test(sdk-pinocchio): add invoke_with_fee_payer handlers and tests Add separate fee_payer support (non-PDA authority) to the pinocchio test program and integration tests: - src: add process_*_invoke_with_fee_payer handlers for approve, revoke, transfer, burn, ctoken_mint_to; add InstructionType variants 36-40 and dispatch in lib.rs - tests: fix invoke_signed tests to include fee_payer account; add *_invoke_with_separate_fee_payer tests for all five operations demonstrating authority != fee_payer separation - pda_owner accounts in invoke_signed tests changed to new_readonly --- .../sdk-light-token-pinocchio/src/approve.rs | 35 +++- .../sdk-light-token-pinocchio/src/burn.rs | 30 ++++ .../src/ctoken_mint_to.rs | 30 ++++ .../sdk-light-token-pinocchio/src/lib.rs | 59 ++++++- .../sdk-light-token-pinocchio/src/revoke.rs | 29 ++- .../sdk-light-token-pinocchio/src/transfer.rs | 29 +++ .../tests/test_approve_revoke.rs | 166 +++++++++++++++++- .../tests/test_burn.rs | 67 +++++++ .../tests/test_ctoken_mint_to.rs | 80 +++++++++ .../tests/test_transfer.rs | 62 +++++++ 10 files changed, 575 insertions(+), 12 deletions(-) diff --git a/sdk-tests/sdk-light-token-pinocchio/src/approve.rs b/sdk-tests/sdk-light-token-pinocchio/src/approve.rs index 001b87be00..539adeac90 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/approve.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/approve.rs @@ -51,11 +51,12 @@ pub fn process_approve_invoke( /// - accounts[2]: PDA owner (program signs) /// - accounts[3]: system_program /// - accounts[4]: light_token_program +/// - accounts[5]: fee_payer (writable, signer) pub fn process_approve_invoke_signed( accounts: &[AccountInfo], data: ApproveData, ) -> Result<(), ProgramError> { - if accounts.len() < 5 { + if accounts.len() < 6 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -75,9 +76,39 @@ pub fn process_approve_invoke_signed( owner: &accounts[2], system_program: &accounts[3], amount: data.amount, - fee_payer: &accounts[2], + fee_payer: &accounts[5], } .invoke_signed(&[signer])?; Ok(()) } + +/// Handler for approving a delegate with a separate fee_payer (invoke) +/// +/// Account order: +/// - accounts[0]: token_account (writable) +/// - accounts[1]: delegate +/// - accounts[2]: owner (signer) +/// - accounts[3]: system_program +/// - accounts[4]: light_token_program +/// - accounts[5]: fee_payer (writable, signer) +pub fn process_approve_invoke_with_fee_payer( + accounts: &[AccountInfo], + data: ApproveData, +) -> Result<(), ProgramError> { + if accounts.len() < 6 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + ApproveCpi { + token_account: &accounts[0], + delegate: &accounts[1], + owner: &accounts[2], + system_program: &accounts[3], + amount: data.amount, + fee_payer: &accounts[5], + } + .invoke()?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/burn.rs b/sdk-tests/sdk-light-token-pinocchio/src/burn.rs index c4d8c6dde8..6a70debef0 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/burn.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/burn.rs @@ -80,3 +80,33 @@ pub fn process_burn_invoke_signed( Ok(()) } + +/// Handler for burning CTokens with a separate fee_payer (invoke) +/// +/// Account order: +/// - accounts[0]: source (Light Token account, writable) +/// - accounts[1]: mint (writable) +/// - accounts[2]: authority (owner, signer) +/// - accounts[3]: light_token_program +/// - accounts[4]: system_program +/// - accounts[5]: fee_payer (writable, signer) +pub fn process_burn_invoke_with_fee_payer( + accounts: &[AccountInfo], + amount: u64, +) -> Result<(), ProgramError> { + if accounts.len() < 6 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + BurnCpi { + source: &accounts[0], + mint: &accounts[1], + amount, + authority: &accounts[2], + system_program: &accounts[4], + fee_payer: &accounts[5], + } + .invoke()?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/ctoken_mint_to.rs b/sdk-tests/sdk-light-token-pinocchio/src/ctoken_mint_to.rs index 8f53668856..1492dbefe0 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/ctoken_mint_to.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/ctoken_mint_to.rs @@ -81,3 +81,33 @@ pub fn process_mint_to_invoke_signed( Ok(()) } + +/// Handler for minting to Token with a separate fee_payer (invoke) +/// +/// Account order: +/// - accounts[0]: mint (writable) +/// - accounts[1]: destination (Token account, writable) +/// - accounts[2]: authority (mint authority, signer) +/// - accounts[3]: system_program +/// - accounts[4]: light_token_program +/// - accounts[5]: fee_payer (writable, signer) +pub fn process_mint_to_invoke_with_fee_payer( + accounts: &[AccountInfo], + amount: u64, +) -> Result<(), ProgramError> { + if accounts.len() < 6 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + MintToCpi { + mint: &accounts[0], + destination: &accounts[1], + amount, + authority: &accounts[2], + system_program: &accounts[3], + fee_payer: &accounts[5], + } + .invoke()?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/lib.rs b/sdk-tests/sdk-light-token-pinocchio/src/lib.rs index e3abcde6ed..1f1aed05cb 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/lib.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/lib.rs @@ -19,8 +19,11 @@ mod transfer_interface; mod transfer_spl_ctoken; // Re-export all instruction data types -pub use approve::{process_approve_invoke, process_approve_invoke_signed, ApproveData}; -pub use burn::{process_burn_invoke, process_burn_invoke_signed, BurnData}; +pub use approve::{ + process_approve_invoke, process_approve_invoke_signed, + process_approve_invoke_with_fee_payer, ApproveData, +}; +pub use burn::{process_burn_invoke, process_burn_invoke_signed, process_burn_invoke_with_fee_payer, BurnData}; pub use close::{process_close_account_invoke, process_close_account_invoke_signed}; pub use create_ata::{process_create_ata_invoke, process_create_ata_invoke_signed, CreateAtaData}; pub use create_mint::{ @@ -31,15 +34,23 @@ pub use create_token_account::{ process_create_token_account_invoke, process_create_token_account_invoke_signed, CreateTokenAccountData, }; -pub use ctoken_mint_to::{process_mint_to_invoke, process_mint_to_invoke_signed, MintToData}; +pub use ctoken_mint_to::{ + process_mint_to_invoke, process_mint_to_invoke_signed, process_mint_to_invoke_with_fee_payer, + MintToData, +}; pub use freeze::{process_freeze_invoke, process_freeze_invoke_signed}; use light_macros::pubkey_array; use pinocchio::{ account_info::AccountInfo, entrypoint, program_error::ProgramError, ProgramResult, }; -pub use revoke::{process_revoke_invoke, process_revoke_invoke_signed}; +pub use revoke::{ + process_revoke_invoke, process_revoke_invoke_signed, process_revoke_invoke_with_fee_payer, +}; pub use thaw::{process_thaw_invoke, process_thaw_invoke_signed}; -pub use transfer::{process_transfer_invoke, process_transfer_invoke_signed, TransferData}; +pub use transfer::{ + process_transfer_invoke, process_transfer_invoke_signed, + process_transfer_invoke_with_fee_payer, TransferData, +}; pub use transfer_checked::{ process_transfer_checked_invoke, process_transfer_checked_invoke_signed, TransferCheckedData, }; @@ -134,6 +145,16 @@ pub enum InstructionType { CTokenTransferCheckedInvoke = 34, /// Transfer cTokens with checked decimals from PDA-owned account (invoke_signed) CTokenTransferCheckedInvokeSigned = 35, + /// Transfer cTokens with separate fee_payer (invoke, non-PDA authority) + CTokenTransferInvokeWithFeePayer = 36, + /// Burn CTokens with separate fee_payer (invoke, non-PDA authority) + BurnInvokeWithFeePayer = 37, + /// Mint to Light Token with separate fee_payer (invoke, non-PDA authority) + CTokenMintToInvokeWithFeePayer = 38, + /// Approve delegate with separate fee_payer (invoke, non-PDA authority) + ApproveInvokeWithFeePayer = 39, + /// Revoke delegation with separate fee_payer (invoke, non-PDA authority) + RevokeInvokeWithFeePayer = 40, } impl TryFrom for InstructionType { @@ -174,6 +195,11 @@ impl TryFrom for InstructionType { 32 => Ok(InstructionType::CTokenMintToInvokeSigned), 34 => Ok(InstructionType::CTokenTransferCheckedInvoke), 35 => Ok(InstructionType::CTokenTransferCheckedInvokeSigned), + 36 => Ok(InstructionType::CTokenTransferInvokeWithFeePayer), + 37 => Ok(InstructionType::BurnInvokeWithFeePayer), + 38 => Ok(InstructionType::CTokenMintToInvokeWithFeePayer), + 39 => Ok(InstructionType::ApproveInvokeWithFeePayer), + 40 => Ok(InstructionType::RevokeInvokeWithFeePayer), _ => Err(ProgramError::InvalidInstructionData), } } @@ -321,6 +347,29 @@ pub fn process_instruction( .map_err(|_| ProgramError::InvalidInstructionData)?; process_transfer_checked_invoke_signed(accounts, data) } + InstructionType::CTokenTransferInvokeWithFeePayer => { + let data = TransferData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_transfer_invoke_with_fee_payer(accounts, data) + } + InstructionType::BurnInvokeWithFeePayer => { + let data = BurnData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_burn_invoke_with_fee_payer(accounts, data.amount) + } + InstructionType::CTokenMintToInvokeWithFeePayer => { + let data = MintToData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_mint_to_invoke_with_fee_payer(accounts, data.amount) + } + InstructionType::ApproveInvokeWithFeePayer => { + let data = ApproveData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_approve_invoke_with_fee_payer(accounts, data) + } + InstructionType::RevokeInvokeWithFeePayer => { + process_revoke_invoke_with_fee_payer(accounts) + } _ => Err(ProgramError::InvalidInstructionData), } } diff --git a/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs b/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs index c1ddf7ff93..aa0ec2bc0f 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs @@ -37,8 +37,9 @@ pub fn process_revoke_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramErro /// - accounts[1]: PDA owner (program signs) /// - accounts[2]: system_program /// - accounts[3]: light_token_program +/// - accounts[4]: fee_payer (writable, signer) pub fn process_revoke_invoke_signed(accounts: &[AccountInfo]) -> Result<(), ProgramError> { - if accounts.len() < 4 { + if accounts.len() < 5 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -58,9 +59,33 @@ pub fn process_revoke_invoke_signed(accounts: &[AccountInfo]) -> Result<(), Prog token_account: &accounts[0], owner: &accounts[1], system_program: &accounts[2], - fee_payer: &accounts[1], + fee_payer: &accounts[4], } .invoke_signed(&[signer])?; Ok(()) } + +/// Handler for revoking delegation with a separate fee_payer (invoke) +/// +/// Account order: +/// - accounts[0]: token_account (writable) +/// - accounts[1]: owner (signer) +/// - accounts[2]: system_program +/// - accounts[3]: light_token_program +/// - accounts[4]: fee_payer (writable, signer) +pub fn process_revoke_invoke_with_fee_payer(accounts: &[AccountInfo]) -> Result<(), ProgramError> { + if accounts.len() < 5 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + RevokeCpi { + token_account: &accounts[0], + owner: &accounts[1], + system_program: &accounts[2], + fee_payer: &accounts[4], + } + .invoke()?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/transfer.rs b/sdk-tests/sdk-light-token-pinocchio/src/transfer.rs index bc3aff46c1..5458cc5c8b 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/transfer.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/transfer.rs @@ -94,3 +94,32 @@ pub fn process_transfer_invoke_signed( Ok(()) } + +/// Handler for transferring compressed tokens with a separate fee_payer (invoke) +/// +/// Account order: +/// - accounts[0]: source ctoken account +/// - accounts[1]: destination ctoken account +/// - accounts[2]: authority (signer) +/// - accounts[3]: system_program +/// - accounts[4]: fee_payer (writable, signer) +pub fn process_transfer_invoke_with_fee_payer( + accounts: &[AccountInfo], + data: TransferData, +) -> Result<(), ProgramError> { + if accounts.len() < 5 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + TransferCpi { + source: &accounts[0], + destination: &accounts[1], + amount: data.amount, + authority: &accounts[2], + system_program: &accounts[3], + fee_payer: &accounts[4], + } + .invoke()?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_approve_revoke.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_approve_revoke.rs index 1e22002a91..97ceef7e4d 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/test_approve_revoke.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_approve_revoke.rs @@ -114,9 +114,10 @@ async fn test_approve_invoke_signed() { accounts: vec![ AccountMeta::new(ata, false), // token_account AccountMeta::new_readonly(delegate.pubkey(), false), // delegate - AccountMeta::new(pda_owner, false), // PDA owner (program signs) + AccountMeta::new_readonly(pda_owner, false), // PDA owner (program signs, readonly) AccountMeta::new_readonly(Pubkey::default(), false), // system_program AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new(payer.pubkey(), true), // fee_payer ], data: instruction_data, }; @@ -268,9 +269,10 @@ async fn test_revoke_invoke_signed() { accounts: vec![ AccountMeta::new(ata, false), AccountMeta::new_readonly(delegate.pubkey(), false), - AccountMeta::new(pda_owner, false), + AccountMeta::new_readonly(pda_owner, false), // PDA owner (program signs, readonly) AccountMeta::new_readonly(Pubkey::default(), false), AccountMeta::new_readonly(light_token_program, false), + AccountMeta::new(payer.pubkey(), true), // fee_payer ], data: approve_instruction_data, }; @@ -295,9 +297,10 @@ async fn test_revoke_invoke_signed() { program_id: PROGRAM_ID, accounts: vec![ AccountMeta::new(ata, false), // token_account - AccountMeta::new(pda_owner, false), // PDA owner (program signs) + AccountMeta::new_readonly(pda_owner, false), // PDA owner (program signs, readonly) AccountMeta::new_readonly(Pubkey::default(), false), // system_program AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new(payer.pubkey(), true), // fee_payer ], data: revoke_instruction_data, }; @@ -319,3 +322,160 @@ async fn test_revoke_invoke_signed() { "Delegated amount should be 0 after revoke" ); } + +/// Test approving a delegate with a separate fee_payer using ApproveCTokenCpi::invoke() +#[tokio::test] +async fn test_approve_invoke_with_separate_fee_payer() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Separate keypair as the token account owner (not the fee payer) + let owner_keypair = Keypair::new(); + + let (_mint_pda, _compression_address, ata_pubkeys, _mint_seed) = setup_create_mint( + &mut rpc, + &payer, + payer.pubkey(), + 9, + vec![(1000, owner_keypair.pubkey())], + ) + .await; + + let ata = ata_pubkeys[0]; + let delegate = Keypair::new(); + let approve_amount = 100u64; + + let mut instruction_data = vec![InstructionType::ApproveInvokeWithFeePayer as u8]; + let approve_data = ApproveData { + amount: approve_amount, + }; + approve_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(ata, false), // token_account + AccountMeta::new_readonly(delegate.pubkey(), false), // delegate + AccountMeta::new_readonly(owner_keypair.pubkey(), true), // owner (readonly signer) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new(payer.pubkey(), true), // fee_payer (separate) + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &owner_keypair]) + .await + .unwrap(); + + let ata_account = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken = Token::deserialize(&mut &ata_account.data[..]).unwrap(); + + assert_eq!( + ctoken.delegate, + Some(delegate.pubkey().to_bytes().into()), + "Delegate should be set after approve with separate fee payer" + ); + assert_eq!( + ctoken.delegated_amount, approve_amount, + "Delegated amount should match" + ); +} + +/// Test revoking delegation with a separate fee_payer using RevokeCTokenCpi::invoke() +#[tokio::test] +async fn test_revoke_invoke_with_separate_fee_payer() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Separate keypair as the token account owner (not the fee payer) + let owner_keypair = Keypair::new(); + + let (_mint_pda, _compression_address, ata_pubkeys, _mint_seed) = setup_create_mint( + &mut rpc, + &payer, + payer.pubkey(), + 9, + vec![(1000, owner_keypair.pubkey())], + ) + .await; + + let ata = ata_pubkeys[0]; + let delegate = Keypair::new(); + let approve_amount = 100u64; + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + + // First approve a delegate (owner signs, payer pays) + let mut approve_instruction_data = vec![InstructionType::ApproveInvokeWithFeePayer as u8]; + let approve_data = ApproveData { + amount: approve_amount, + }; + approve_data + .serialize(&mut approve_instruction_data) + .unwrap(); + + let approve_instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(ata, false), + AccountMeta::new_readonly(delegate.pubkey(), false), + AccountMeta::new_readonly(owner_keypair.pubkey(), true), + AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new_readonly(light_token_program, false), + AccountMeta::new(payer.pubkey(), true), + ], + data: approve_instruction_data, + }; + + rpc.create_and_send_transaction( + &[approve_instruction], + &payer.pubkey(), + &[&payer, &owner_keypair], + ) + .await + .unwrap(); + + // Revoke with separate fee_payer + let revoke_instruction_data = vec![InstructionType::RevokeInvokeWithFeePayer as u8]; + + let revoke_instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(ata, false), // token_account + AccountMeta::new_readonly(owner_keypair.pubkey(), true), // owner (readonly signer) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new(payer.pubkey(), true), // fee_payer (separate) + ], + data: revoke_instruction_data, + }; + + rpc.create_and_send_transaction( + &[revoke_instruction], + &payer.pubkey(), + &[&payer, &owner_keypair], + ) + .await + .unwrap(); + + let ata_account = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken = Token::deserialize(&mut &ata_account.data[..]).unwrap(); + + assert_eq!( + ctoken.delegate, None, + "Delegate should be cleared after revoke with separate fee payer" + ); + assert_eq!( + ctoken.delegated_amount, 0, + "Delegated amount should be 0 after revoke" + ); +} diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_burn.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_burn.rs index dd2a1814d3..21f4614175 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/test_burn.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_burn.rs @@ -150,3 +150,70 @@ async fn test_burn_invoke_signed() { "Light Token should match expected state after burn" ); } + +/// Test burning CTokens with a separate fee_payer using BurnCTokenCpi::invoke() +#[tokio::test] +async fn test_burn_invoke_with_separate_fee_payer() { + use solana_sdk::signature::Keypair; + + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Separate keypair as the token account owner (not the fee payer) + let owner_keypair = Keypair::new(); + + let (mint_pda, _compression_address, ata_pubkeys) = setup_create_mint_with_freeze_authority( + &mut rpc, + &payer, + payer.pubkey(), + None, + 9, + vec![(1000, owner_keypair.pubkey())], + ) + .await; + + let ata = ata_pubkeys[0]; + let burn_amount = 200u64; + + let ata_account_before = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_before = Token::deserialize(&mut &ata_account_before.data[..]).unwrap(); + + let mut instruction_data = vec![InstructionType::BurnInvokeWithFeePayer as u8]; + let burn_data = BurnData { + amount: burn_amount, + }; + burn_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(ata, false), // source + AccountMeta::new(mint_pda, false), // mint + AccountMeta::new_readonly(owner_keypair.pubkey(), true), // authority (readonly signer) + AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(payer.pubkey(), true), // fee_payer (separate) + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &owner_keypair]) + .await + .unwrap(); + + let ata_account_after = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after = Token::deserialize(&mut &ata_account_after.data[..]).unwrap(); + + let mut expected_ctoken_after = ctoken_before; + expected_ctoken_after.amount = 800; // 1000 - 200 + + assert_eq!( + ctoken_after, expected_ctoken_after, + "Light Token should match expected state after burn with separate fee payer" + ); +} diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs index d6926e8d34..33ab42bdc0 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs @@ -82,6 +82,86 @@ async fn test_ctoken_mint_to_invoke() { ); } +/// Test minting to Light Token with a separate fee_payer using CTokenMintToCpi::invoke() +/// +/// Demonstrates that the mint authority (signer) and fee_payer are separate accounts. +/// Setup uses payer as mint_authority. The actual CPI uses a separate funded fee_payer_keypair. +#[tokio::test] +async fn test_ctoken_mint_to_invoke_with_separate_fee_payer() { + use light_client::rpc::Rpc as _; + use solana_sdk::signature::Keypair; + + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Separate keypair as the fee_payer (not the mint authority). + let fee_payer_keypair = Keypair::new(); + rpc.airdrop_lamports(&fee_payer_keypair.pubkey(), 1_000_000_000) + .await + .unwrap(); + + // Setup with payer as mint_authority (setup signs correctly with payer + mint_seed). + let (mint_pda, _compression_address, ata_pubkeys) = setup_create_mint_with_freeze_authority( + &mut rpc, + &payer, + payer.pubkey(), // payer is the mint authority + None, + 9, + vec![(0, payer.pubkey())], // 0 tokens: skip initial MintTo for clarity + ) + .await; + + let ata = ata_pubkeys[0]; + let mint_amount = 300u64; + + let ata_account_before = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_before = Token::deserialize(&mut &ata_account_before.data[..]).unwrap(); + + let mut instruction_data = vec![InstructionType::CTokenMintToInvokeWithFeePayer as u8]; + let mint_data = MintToData { + amount: mint_amount, + }; + mint_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + let system_program = Pubkey::default(); + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(mint_pda, false), // mint + AccountMeta::new(ata, false), // destination + AccountMeta::new_readonly(payer.pubkey(), true), // authority (readonly signer) + AccountMeta::new_readonly(system_program, false), // system_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new(fee_payer_keypair.pubkey(), true), // fee_payer (separate) + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction( + &[instruction], + &payer.pubkey(), + &[&payer, &fee_payer_keypair], + ) + .await + .unwrap(); + + let ata_account_after = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after = Token::deserialize(&mut &ata_account_after.data[..]).unwrap(); + + let mut expected_ctoken = ctoken_before; + expected_ctoken.amount = 300; // 0 + 300 + + assert_eq!( + ctoken_after, expected_ctoken, + "Light Token should match expected state after mint with separate fee payer" + ); +} + /// Test minting to Light Token with PDA authority using CTokenMintToCpi::invoke_signed() /// /// This test: diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs index 3b40ed07a3..e2e1852721 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs @@ -135,3 +135,65 @@ async fn test_ctoken_transfer_invoke_signed() { let dest_state_after = Token::deserialize(&mut &dest_data_after.data[..]).unwrap(); assert_eq!(dest_state_after.amount, 300); } + +/// Test CTokenTransfer with a separate fee_payer using invoke() +#[tokio::test] +async fn test_ctoken_transfer_invoke_with_separate_fee_payer() { + use light_token_interface::state::Token; + use solana_sdk::signature::Keypair; + + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Separate keypair as the source account owner (not the fee payer) + let owner_keypair = Keypair::new(); + let dest_owner = payer.pubkey(); + + let (_mint_pda, _compression_address, ata_pubkeys, _mint_seed) = setup_create_mint( + &mut rpc, + &payer, + payer.pubkey(), + 9, + vec![(1000, owner_keypair.pubkey()), (0, dest_owner)], + ) + .await; + + let source_ata = ata_pubkeys[0]; + let dest_ata = ata_pubkeys[1]; + + let transfer_data = TransferData { amount: 400 }; + let instruction_data = [ + vec![InstructionType::CTokenTransferInvokeWithFeePayer as u8], + transfer_data.try_to_vec().unwrap(), + ] + .concat(); + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(source_ata, false), + AccountMeta::new(dest_ata, false), + AccountMeta::new_readonly(owner_keypair.pubkey(), true), // authority (readonly signer) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(payer.pubkey(), true), // fee_payer (separate) + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &owner_keypair]) + .await + .unwrap(); + + let source_data_after = rpc.get_account(source_ata).await.unwrap().unwrap(); + let source_state_after = Token::deserialize(&mut &source_data_after.data[..]).unwrap(); + assert_eq!(source_state_after.amount, 600); + + let dest_data_after = rpc.get_account(dest_ata).await.unwrap().unwrap(); + let dest_state_after = Token::deserialize(&mut &dest_data_after.data[..]).unwrap(); + assert_eq!(dest_state_after.amount, 400); +} From 4bb8746ec9e57287512e61866ae1eac6ccf7a9b3 Mon Sep 17 00:00:00 2001 From: ananas Date: Fri, 20 Feb 2026 21:08:11 +0000 Subject: [PATCH 19/26] fix(sdk-pinocchio-test): add fee_payer to transfer_checked tests The TransferCheckedCpi handler was updated to require a separate fee_payer at accounts[5], but the three transfer_checked tests still passed light_token_program there, causing PrivilegeEscalation. Add payer as writable fee_payer at [5] and move light_token_program to [6] in all three test_ctoken_transfer_checked_* tests. --- .../sdk-light-token-pinocchio/tests/test_transfer_checked.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_checked.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_checked.rs index 6282b96d4e..38a2de75e1 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_checked.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_checked.rs @@ -156,6 +156,7 @@ async fn test_ctoken_transfer_checked_spl_mint() { AccountMeta::new(dest_ata, false), AccountMeta::new_readonly(source_owner, true), AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(payer.pubkey(), true), // fee_payer AccountMeta::new_readonly(light_token_program, false), ], data: instruction_data, @@ -265,6 +266,7 @@ async fn test_ctoken_transfer_checked_t22_mint() { AccountMeta::new(dest_ata, false), AccountMeta::new_readonly(source_owner, true), AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(payer.pubkey(), true), // fee_payer AccountMeta::new_readonly(light_token_program, false), ], data: instruction_data, @@ -329,6 +331,7 @@ async fn test_ctoken_transfer_checked_mint() { AccountMeta::new(dest_ata, false), AccountMeta::new_readonly(source_owner, true), AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(payer.pubkey(), true), // fee_payer AccountMeta::new_readonly(light_token_program, false), ], data: instruction_data, From d703b2500a168eeee5b15efd4f488a32b4fa334a Mon Sep 17 00:00:00 2001 From: ananas Date: Fri, 20 Feb 2026 21:17:56 +0000 Subject: [PATCH 20/26] test(sdk-test): add invoke_with_fee_payer handlers and tests - Fix approve/revoke invoke_signed: add separate fee_payer at accounts[5]/[4] - Add process_{transfer,burn,mint_to,approve,revoke}_invoke_with_fee_payer handlers - Add discriminators 36-40 to InstructionType enum and dispatch - Add test_{transfer,burn,mint_to,approve,revoke}_invoke_with_separate_fee_payer tests --- sdk-tests/sdk-light-token-test/src/approve.rs | 35 +++- sdk-tests/sdk-light-token-test/src/burn.rs | 30 ++++ .../src/ctoken_mint_to.rs | 30 ++++ sdk-tests/sdk-light-token-test/src/lib.rs | 88 +++++++++- sdk-tests/sdk-light-token-test/src/revoke.rs | 29 +++- .../sdk-light-token-test/src/transfer.rs | 29 ++++ .../tests/test_approve_revoke.rs | 164 +++++++++++++++++- .../sdk-light-token-test/tests/test_burn.rs | 65 +++++++ .../tests/test_ctoken_mint_to.rs | 70 ++++++++ .../tests/test_transfer.rs | 62 +++++++ 10 files changed, 589 insertions(+), 13 deletions(-) diff --git a/sdk-tests/sdk-light-token-test/src/approve.rs b/sdk-tests/sdk-light-token-test/src/approve.rs index 2f5a97768e..4987081344 100644 --- a/sdk-tests/sdk-light-token-test/src/approve.rs +++ b/sdk-tests/sdk-light-token-test/src/approve.rs @@ -47,11 +47,12 @@ pub fn process_approve_invoke( /// - accounts[2]: PDA owner (program signs) /// - accounts[3]: system_program /// - accounts[4]: light_token_program +/// - accounts[5]: fee_payer (writable, signer) pub fn process_approve_invoke_signed( accounts: &[AccountInfo], data: ApproveData, ) -> Result<(), ProgramError> { - if accounts.len() < 5 { + if accounts.len() < 6 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -70,9 +71,39 @@ pub fn process_approve_invoke_signed( owner: accounts[2].clone(), system_program: accounts[3].clone(), amount: data.amount, - fee_payer: accounts[2].clone(), + fee_payer: accounts[5].clone(), } .invoke_signed(&[signer_seeds])?; Ok(()) } + +/// Handler for approving a delegate with a separate fee_payer (invoke) +/// +/// Account order: +/// - accounts[0]: token_account (writable) +/// - accounts[1]: delegate +/// - accounts[2]: owner (signer) +/// - accounts[3]: system_program +/// - accounts[4]: light_token_program +/// - accounts[5]: fee_payer (writable, signer) +pub fn process_approve_invoke_with_fee_payer( + accounts: &[AccountInfo], + data: ApproveData, +) -> Result<(), ProgramError> { + if accounts.len() < 6 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + ApproveCpi { + token_account: accounts[0].clone(), + delegate: accounts[1].clone(), + owner: accounts[2].clone(), + system_program: accounts[3].clone(), + amount: data.amount, + fee_payer: accounts[5].clone(), + } + .invoke()?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-test/src/burn.rs b/sdk-tests/sdk-light-token-test/src/burn.rs index f2a2a9e4a4..0daa2d0ab9 100644 --- a/sdk-tests/sdk-light-token-test/src/burn.rs +++ b/sdk-tests/sdk-light-token-test/src/burn.rs @@ -74,3 +74,33 @@ pub fn process_burn_invoke_signed( Ok(()) } + +/// Handler for burning CTokens with a separate fee_payer (invoke) +/// +/// Account order: +/// - accounts[0]: source (Light Token account, writable) +/// - accounts[1]: mint (writable) +/// - accounts[2]: authority (owner, signer) +/// - accounts[3]: light_token_program +/// - accounts[4]: system_program +/// - accounts[5]: fee_payer (writable, signer) +pub fn process_burn_invoke_with_fee_payer( + accounts: &[AccountInfo], + amount: u64, +) -> Result<(), ProgramError> { + if accounts.len() < 6 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + BurnCpi { + source: accounts[0].clone(), + mint: accounts[1].clone(), + amount, + authority: accounts[2].clone(), + system_program: accounts[4].clone(), + fee_payer: accounts[5].clone(), + } + .invoke()?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-test/src/ctoken_mint_to.rs b/sdk-tests/sdk-light-token-test/src/ctoken_mint_to.rs index fc8a72ee33..a8e47537c7 100644 --- a/sdk-tests/sdk-light-token-test/src/ctoken_mint_to.rs +++ b/sdk-tests/sdk-light-token-test/src/ctoken_mint_to.rs @@ -74,3 +74,33 @@ pub fn process_mint_to_invoke_signed( Ok(()) } + +/// Handler for minting to Token with a separate fee_payer (invoke) +/// +/// Account order: +/// - accounts[0]: mint (writable) +/// - accounts[1]: destination (Token account, writable) +/// - accounts[2]: authority (mint authority, signer) +/// - accounts[3]: system_program +/// - accounts[4]: light_token_program +/// - accounts[5]: fee_payer (writable, signer) +pub fn process_mint_to_invoke_with_fee_payer( + accounts: &[AccountInfo], + amount: u64, +) -> Result<(), ProgramError> { + if accounts.len() < 6 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + MintToCpi { + mint: accounts[0].clone(), + destination: accounts[1].clone(), + amount, + authority: accounts[2].clone(), + system_program: accounts[3].clone(), + fee_payer: accounts[5].clone(), + } + .invoke()?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-test/src/lib.rs b/sdk-tests/sdk-light-token-test/src/lib.rs index 21179b2e09..50ca5281a7 100644 --- a/sdk-tests/sdk-light-token-test/src/lib.rs +++ b/sdk-tests/sdk-light-token-test/src/lib.rs @@ -17,8 +17,13 @@ mod transfer_interface; mod transfer_spl_ctoken; // Re-export all instruction data types -pub use approve::{process_approve_invoke, process_approve_invoke_signed, ApproveData}; -pub use burn::{process_burn_invoke, process_burn_invoke_signed, BurnData}; +pub use approve::{ + process_approve_invoke, process_approve_invoke_signed, process_approve_invoke_with_fee_payer, + ApproveData, +}; +pub use burn::{ + process_burn_invoke, process_burn_invoke_signed, process_burn_invoke_with_fee_payer, BurnData, +}; pub use close::{process_close_account_invoke, process_close_account_invoke_signed}; pub use create_ata::{process_create_ata_invoke, process_create_ata_invoke_signed, CreateAtaData}; pub use create_mint::{ @@ -29,15 +34,23 @@ pub use create_token_account::{ process_create_token_account_invoke, process_create_token_account_invoke_signed, CreateTokenAccountData, }; -pub use ctoken_mint_to::{process_mint_to_invoke, process_mint_to_invoke_signed, MintToData}; +pub use ctoken_mint_to::{ + process_mint_to_invoke, process_mint_to_invoke_signed, process_mint_to_invoke_with_fee_payer, + MintToData, +}; pub use decompress_mint::{process_decompress_mint_invoke_signed, DecompressCmintData}; pub use freeze::{process_freeze_invoke, process_freeze_invoke_signed}; -pub use revoke::{process_revoke_invoke, process_revoke_invoke_signed}; +pub use revoke::{ + process_revoke_invoke, process_revoke_invoke_signed, process_revoke_invoke_with_fee_payer, +}; use solana_program::{ account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey, pubkey::Pubkey, }; pub use thaw::{process_thaw_invoke, process_thaw_invoke_signed}; -pub use transfer::{process_transfer_invoke, process_transfer_invoke_signed, TransferData}; +pub use transfer::{ + process_transfer_invoke, process_transfer_invoke_signed, process_transfer_invoke_with_fee_payer, + TransferData, +}; pub use transfer_checked::{ process_transfer_checked_invoke, process_transfer_checked_invoke_signed, TransferCheckedData, }; @@ -134,6 +147,16 @@ pub enum InstructionType { CTokenTransferCheckedInvoke = 34, /// Transfer cTokens with checked decimals from PDA-owned account (invoke_signed) CTokenTransferCheckedInvokeSigned = 35, + /// Transfer compressed tokens with separate fee_payer (invoke) + CTokenTransferInvokeWithFeePayer = 36, + /// Burn CTokens with separate fee_payer (invoke) + BurnInvokeWithFeePayer = 37, + /// Mint to Light Token with separate fee_payer (invoke) + CTokenMintToInvokeWithFeePayer = 38, + /// Approve delegate with separate fee_payer (invoke) + ApproveInvokeWithFeePayer = 39, + /// Revoke delegation with separate fee_payer (invoke) + RevokeInvokeWithFeePayer = 40, } impl TryFrom for InstructionType { @@ -175,6 +198,11 @@ impl TryFrom for InstructionType { 33 => Ok(InstructionType::DecompressCmintInvokeSigned), 34 => Ok(InstructionType::CTokenTransferCheckedInvoke), 35 => Ok(InstructionType::CTokenTransferCheckedInvokeSigned), + 36 => Ok(InstructionType::CTokenTransferInvokeWithFeePayer), + 37 => Ok(InstructionType::BurnInvokeWithFeePayer), + 38 => Ok(InstructionType::CTokenMintToInvokeWithFeePayer), + 39 => Ok(InstructionType::ApproveInvokeWithFeePayer), + 40 => Ok(InstructionType::RevokeInvokeWithFeePayer), _ => Err(ProgramError::InvalidInstructionData), } } @@ -327,6 +355,29 @@ pub fn process_instruction( .map_err(|_| ProgramError::InvalidInstructionData)?; process_transfer_checked_invoke_signed(accounts, data) } + InstructionType::CTokenTransferInvokeWithFeePayer => { + let data = TransferData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_transfer_invoke_with_fee_payer(accounts, data) + } + InstructionType::BurnInvokeWithFeePayer => { + let data = BurnData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_burn_invoke_with_fee_payer(accounts, data.amount) + } + InstructionType::CTokenMintToInvokeWithFeePayer => { + let data = MintToData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_mint_to_invoke_with_fee_payer(accounts, data.amount) + } + InstructionType::ApproveInvokeWithFeePayer => { + let data = ApproveData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_approve_invoke_with_fee_payer(accounts, data) + } + InstructionType::RevokeInvokeWithFeePayer => { + process_revoke_invoke_with_fee_payer(accounts) + } _ => Err(ProgramError::InvalidInstructionData), } } @@ -371,6 +422,11 @@ mod tests { assert_eq!(InstructionType::DecompressCmintInvokeSigned as u8, 33); assert_eq!(InstructionType::CTokenTransferCheckedInvoke as u8, 34); assert_eq!(InstructionType::CTokenTransferCheckedInvokeSigned as u8, 35); + assert_eq!(InstructionType::CTokenTransferInvokeWithFeePayer as u8, 36); + assert_eq!(InstructionType::BurnInvokeWithFeePayer as u8, 37); + assert_eq!(InstructionType::CTokenMintToInvokeWithFeePayer as u8, 38); + assert_eq!(InstructionType::ApproveInvokeWithFeePayer as u8, 39); + assert_eq!(InstructionType::RevokeInvokeWithFeePayer as u8, 40); } #[test] @@ -513,6 +569,26 @@ mod tests { InstructionType::try_from(35).unwrap(), InstructionType::CTokenTransferCheckedInvokeSigned ); - assert!(InstructionType::try_from(36).is_err()); + assert_eq!( + InstructionType::try_from(36).unwrap(), + InstructionType::CTokenTransferInvokeWithFeePayer + ); + assert_eq!( + InstructionType::try_from(37).unwrap(), + InstructionType::BurnInvokeWithFeePayer + ); + assert_eq!( + InstructionType::try_from(38).unwrap(), + InstructionType::CTokenMintToInvokeWithFeePayer + ); + assert_eq!( + InstructionType::try_from(39).unwrap(), + InstructionType::ApproveInvokeWithFeePayer + ); + assert_eq!( + InstructionType::try_from(40).unwrap(), + InstructionType::RevokeInvokeWithFeePayer + ); + assert!(InstructionType::try_from(41).is_err()); } } diff --git a/sdk-tests/sdk-light-token-test/src/revoke.rs b/sdk-tests/sdk-light-token-test/src/revoke.rs index 47fae3d41a..9ce58c0adc 100644 --- a/sdk-tests/sdk-light-token-test/src/revoke.rs +++ b/sdk-tests/sdk-light-token-test/src/revoke.rs @@ -33,8 +33,9 @@ pub fn process_revoke_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramErro /// - accounts[1]: PDA owner (program signs) /// - accounts[2]: system_program /// - accounts[3]: light_token_program +/// - accounts[4]: fee_payer (writable, signer) pub fn process_revoke_invoke_signed(accounts: &[AccountInfo]) -> Result<(), ProgramError> { - if accounts.len() < 4 { + if accounts.len() < 5 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -51,9 +52,33 @@ pub fn process_revoke_invoke_signed(accounts: &[AccountInfo]) -> Result<(), Prog token_account: accounts[0].clone(), owner: accounts[1].clone(), system_program: accounts[2].clone(), - fee_payer: accounts[1].clone(), + fee_payer: accounts[4].clone(), } .invoke_signed(&[signer_seeds])?; Ok(()) } + +/// Handler for revoking delegation with a separate fee_payer (invoke) +/// +/// Account order: +/// - accounts[0]: token_account (writable) +/// - accounts[1]: owner (signer) +/// - accounts[2]: system_program +/// - accounts[3]: light_token_program +/// - accounts[4]: fee_payer (writable, signer) +pub fn process_revoke_invoke_with_fee_payer(accounts: &[AccountInfo]) -> Result<(), ProgramError> { + if accounts.len() < 5 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + RevokeCpi { + token_account: accounts[0].clone(), + owner: accounts[1].clone(), + system_program: accounts[2].clone(), + fee_payer: accounts[4].clone(), + } + .invoke()?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-test/src/transfer.rs b/sdk-tests/sdk-light-token-test/src/transfer.rs index e38d086893..5597996e40 100644 --- a/sdk-tests/sdk-light-token-test/src/transfer.rs +++ b/sdk-tests/sdk-light-token-test/src/transfer.rs @@ -88,3 +88,32 @@ pub fn process_transfer_invoke_signed( Ok(()) } + +/// Handler for transferring compressed tokens with a separate fee_payer (invoke) +/// +/// Account order: +/// - accounts[0]: source ctoken account +/// - accounts[1]: destination ctoken account +/// - accounts[2]: authority (signer) +/// - accounts[3]: system_program +/// - accounts[4]: fee_payer (writable, signer) +pub fn process_transfer_invoke_with_fee_payer( + accounts: &[AccountInfo], + data: TransferData, +) -> Result<(), ProgramError> { + if accounts.len() < 5 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + TransferCpi { + source: accounts[0].clone(), + destination: accounts[1].clone(), + amount: data.amount, + authority: accounts[2].clone(), + system_program: accounts[3].clone(), + fee_payer: accounts[4].clone(), + } + .invoke()?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-test/tests/test_approve_revoke.rs b/sdk-tests/sdk-light-token-test/tests/test_approve_revoke.rs index 300108b37c..58a8dcec19 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_approve_revoke.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_approve_revoke.rs @@ -108,9 +108,10 @@ async fn test_approve_invoke_signed() { accounts: vec![ AccountMeta::new(ata, false), // token_account AccountMeta::new_readonly(delegate.pubkey(), false), // delegate - AccountMeta::new(pda_owner, false), // PDA owner (program signs) + AccountMeta::new_readonly(pda_owner, false), // PDA owner (program signs) AccountMeta::new_readonly(Pubkey::default(), false), // system_program AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new(payer.pubkey(), true), // fee_payer ], data: instruction_data, }; @@ -256,9 +257,10 @@ async fn test_revoke_invoke_signed() { accounts: vec![ AccountMeta::new(ata, false), AccountMeta::new_readonly(delegate.pubkey(), false), - AccountMeta::new(pda_owner, false), + AccountMeta::new_readonly(pda_owner, false), AccountMeta::new_readonly(Pubkey::default(), false), AccountMeta::new_readonly(light_token_program, false), + AccountMeta::new(payer.pubkey(), true), // fee_payer ], data: approve_instruction_data, }; @@ -283,9 +285,10 @@ async fn test_revoke_invoke_signed() { program_id: ID, accounts: vec![ AccountMeta::new(ata, false), // token_account - AccountMeta::new(pda_owner, false), // PDA owner (program signs) + AccountMeta::new_readonly(pda_owner, false), // PDA owner (program signs) AccountMeta::new_readonly(Pubkey::default(), false), // system_program AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new(payer.pubkey(), true), // fee_payer ], data: revoke_instruction_data, }; @@ -307,3 +310,158 @@ async fn test_revoke_invoke_signed() { "Delegated amount should be 0 after revoke" ); } + +/// Test approving a delegate with a separate fee_payer using ApproveCTokenCpi::invoke() +#[tokio::test] +async fn test_approve_invoke_with_separate_fee_payer() { + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let owner_keypair = Keypair::new(); + rpc.airdrop_lamports(&owner_keypair.pubkey(), 1_000_000_000) + .await + .unwrap(); + + let (_mint_pda, _compression_address, ata_pubkeys, _mint_seed) = setup_create_mint( + &mut rpc, + &payer, + payer.pubkey(), + 9, + vec![(1000, owner_keypair.pubkey())], + ) + .await; + + let ata = ata_pubkeys[0]; + let delegate = Keypair::new(); + let approve_amount = 100u64; + + let mut instruction_data = vec![InstructionType::ApproveInvokeWithFeePayer as u8]; + let approve_data = ApproveData { + amount: approve_amount, + }; + approve_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + let instruction = Instruction { + program_id: ID, + accounts: vec![ + AccountMeta::new(ata, false), // token_account + AccountMeta::new_readonly(delegate.pubkey(), false), // delegate + AccountMeta::new_readonly(owner_keypair.pubkey(), true), // owner (signer, not fee_payer) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new(payer.pubkey(), true), // fee_payer + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &owner_keypair]) + .await + .unwrap(); + + let ata_account = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken = Token::deserialize(&mut &ata_account.data[..]).unwrap(); + + assert_eq!( + ctoken.delegate, + Some(delegate.pubkey().to_bytes().into()), + "Delegate should be set after approve" + ); + assert_eq!( + ctoken.delegated_amount, approve_amount, + "Delegated amount should match" + ); +} + +/// Test revoking delegation with a separate fee_payer using RevokeCTokenCpi::invoke() +#[tokio::test] +async fn test_revoke_invoke_with_separate_fee_payer() { + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let owner_keypair = Keypair::new(); + rpc.airdrop_lamports(&owner_keypair.pubkey(), 1_000_000_000) + .await + .unwrap(); + + let (_mint_pda, _compression_address, ata_pubkeys, _mint_seed) = setup_create_mint( + &mut rpc, + &payer, + payer.pubkey(), + 9, + vec![(1000, owner_keypair.pubkey())], + ) + .await; + + let ata = ata_pubkeys[0]; + let delegate = Keypair::new(); + let approve_amount = 100u64; + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + + // First approve a delegate + let mut approve_instruction_data = vec![InstructionType::ApproveInvokeWithFeePayer as u8]; + let approve_data = ApproveData { + amount: approve_amount, + }; + approve_data + .serialize(&mut approve_instruction_data) + .unwrap(); + + let approve_instruction = Instruction { + program_id: ID, + accounts: vec![ + AccountMeta::new(ata, false), + AccountMeta::new_readonly(delegate.pubkey(), false), + AccountMeta::new_readonly(owner_keypair.pubkey(), true), + AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new_readonly(light_token_program, false), + AccountMeta::new(payer.pubkey(), true), // fee_payer + ], + data: approve_instruction_data, + }; + + rpc.create_and_send_transaction( + &[approve_instruction], + &payer.pubkey(), + &[&payer, &owner_keypair], + ) + .await + .unwrap(); + + // Now revoke with separate fee_payer + let revoke_instruction_data = vec![InstructionType::RevokeInvokeWithFeePayer as u8]; + + let revoke_instruction = Instruction { + program_id: ID, + accounts: vec![ + AccountMeta::new(ata, false), // token_account + AccountMeta::new_readonly(owner_keypair.pubkey(), true), // owner (signer, not fee_payer) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new(payer.pubkey(), true), // fee_payer + ], + data: revoke_instruction_data, + }; + + rpc.create_and_send_transaction( + &[revoke_instruction], + &payer.pubkey(), + &[&payer, &owner_keypair], + ) + .await + .unwrap(); + + let ata_account_after_revoke = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after_revoke = Token::deserialize(&mut &ata_account_after_revoke.data[..]).unwrap(); + + assert_eq!( + ctoken_after_revoke.delegate, None, + "Delegate should be cleared after revoke" + ); + assert_eq!( + ctoken_after_revoke.delegated_amount, 0, + "Delegated amount should be 0 after revoke" + ); +} diff --git a/sdk-tests/sdk-light-token-test/tests/test_burn.rs b/sdk-tests/sdk-light-token-test/tests/test_burn.rs index 9fc69ba10c..24956fa861 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_burn.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_burn.rs @@ -12,6 +12,7 @@ use shared::*; use solana_sdk::{ instruction::{AccountMeta, Instruction}, pubkey::Pubkey, + signature::Keypair, signer::Signer, }; @@ -144,3 +145,67 @@ async fn test_burn_invoke_signed() { "Light Token should match expected state after burn" ); } + +/// Test burning CTokens with separate fee_payer using BurnCTokenCpi::invoke() +#[tokio::test] +async fn test_burn_invoke_with_separate_fee_payer() { + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let owner_keypair = Keypair::new(); + rpc.airdrop_lamports(&owner_keypair.pubkey(), 1_000_000_000) + .await + .unwrap(); + + let (mint_pda, _compression_address, ata_pubkeys) = setup_create_mint_with_freeze_authority( + &mut rpc, + &payer, + payer.pubkey(), + None, + 9, + vec![(1000, owner_keypair.pubkey())], + ) + .await; + + let ata = ata_pubkeys[0]; + let burn_amount = 200u64; + + let ata_account_before = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_before = Token::deserialize(&mut &ata_account_before.data[..]).unwrap(); + + let mut instruction_data = vec![InstructionType::BurnInvokeWithFeePayer as u8]; + let burn_data = BurnData { + amount: burn_amount, + }; + burn_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + let instruction = Instruction { + program_id: ID, + accounts: vec![ + AccountMeta::new(ata, false), // source + AccountMeta::new(mint_pda, false), // mint + AccountMeta::new_readonly(owner_keypair.pubkey(), true), // authority (signer, not fee_payer) + AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(payer.pubkey(), true), // fee_payer + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &owner_keypair]) + .await + .unwrap(); + + let ata_account_after = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after = Token::deserialize(&mut &ata_account_after.data[..]).unwrap(); + + let mut expected_ctoken = ctoken_before; + expected_ctoken.amount = 800; // 1000 - 200 + + assert_eq!( + ctoken_after, expected_ctoken, + "Light Token should match expected state after burn" + ); +} diff --git a/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs b/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs index 044a38388a..5bd1a70339 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs @@ -252,3 +252,73 @@ async fn test_ctoken_mint_to_invoke_signed() { "Light Token should match expected state after mint" ); } + +/// Test minting to Light Token with separate fee_payer using CTokenMintToCpi::invoke() +#[tokio::test] +async fn test_ctoken_mint_to_invoke_with_separate_fee_payer() { + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let fee_payer_keypair = solana_sdk::signature::Keypair::new(); + rpc.airdrop_lamports(&fee_payer_keypair.pubkey(), 1_000_000_000) + .await + .unwrap(); + + // payer is the mint_authority (setup_create_mint_with_freeze_authority signs with payer) + let (mint_pda, _compression_address, ata_pubkeys) = setup_create_mint_with_freeze_authority( + &mut rpc, + &payer, + payer.pubkey(), // mint authority is payer + None, + 9, + vec![(0, payer.pubkey())], + ) + .await; + + let ata = ata_pubkeys[0]; + let mint_amount = 750u64; + + let ata_account_before = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_before = Token::deserialize(&mut &ata_account_before.data[..]).unwrap(); + + let mut instruction_data = vec![InstructionType::CTokenMintToInvokeWithFeePayer as u8]; + let mint_data = MintToData { + amount: mint_amount, + }; + mint_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + let system_program = Pubkey::default(); + let instruction = Instruction { + program_id: ID, + accounts: vec![ + AccountMeta::new(mint_pda, false), // mint + AccountMeta::new(ata, false), // destination + AccountMeta::new_readonly(payer.pubkey(), true), // authority (signer, not fee_payer) + AccountMeta::new_readonly(system_program, false), // system_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new(fee_payer_keypair.pubkey(), true), // fee_payer + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction( + &[instruction], + &payer.pubkey(), + &[&payer, &fee_payer_keypair], + ) + .await + .unwrap(); + + let ata_account_after = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after = Token::deserialize(&mut &ata_account_after.data[..]).unwrap(); + + let mut expected_ctoken = ctoken_before; + expected_ctoken.amount = 750; // 0 + 750 + + assert_eq!( + ctoken_after, expected_ctoken, + "Light Token should match expected state after mint" + ); +} diff --git a/sdk-tests/sdk-light-token-test/tests/test_transfer.rs b/sdk-tests/sdk-light-token-test/tests/test_transfer.rs index 20394128a1..f064166576 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_transfer.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_transfer.rs @@ -11,6 +11,7 @@ use shared::*; use solana_sdk::{ instruction::{AccountMeta, Instruction}, pubkey::Pubkey, + signature::Keypair, signer::Signer, }; @@ -129,3 +130,64 @@ async fn test_ctoken_transfer_invoke_signed() { let dest_state_after = Token::deserialize(&mut &dest_data_after.data[..]).unwrap(); assert_eq!(dest_state_after.amount, 300); } + +/// Test CTokenTransfer with separate fee_payer using invoke() +#[tokio::test] +async fn test_ctoken_transfer_invoke_with_separate_fee_payer() { + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let owner_keypair = Keypair::new(); + rpc.airdrop_lamports(&owner_keypair.pubkey(), 1_000_000_000) + .await + .unwrap(); + + let dest_owner = Pubkey::new_unique(); + + let (_mint_pda, _compression_address, ata_pubkeys, _mint_seed) = setup_create_mint( + &mut rpc, + &payer, + payer.pubkey(), + 9, + vec![(1000, owner_keypair.pubkey()), (0, dest_owner)], + ) + .await; + + let source_ata = ata_pubkeys[0]; + let dest_ata = ata_pubkeys[1]; + + // Transfer 400 tokens using separate fee_payer + let transfer_data = TransferData { amount: 400 }; + let instruction_data = [ + vec![InstructionType::CTokenTransferInvokeWithFeePayer as u8], + transfer_data.try_to_vec().unwrap(), + ] + .concat(); + + let instruction = Instruction { + program_id: ID, + accounts: vec![ + AccountMeta::new(source_ata, false), + AccountMeta::new(dest_ata, false), + AccountMeta::new_readonly(owner_keypair.pubkey(), true), // authority (signer, not fee_payer) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(payer.pubkey(), true), // fee_payer + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &owner_keypair]) + .await + .unwrap(); + + use light_token_interface::state::Token; + let source_data_after = rpc.get_account(source_ata).await.unwrap().unwrap(); + let source_state_after = Token::deserialize(&mut &source_data_after.data[..]).unwrap(); + assert_eq!(source_state_after.amount, 600); + + let dest_data_after = rpc.get_account(dest_ata).await.unwrap().unwrap(); + let dest_state_after = Token::deserialize(&mut &dest_data_after.data[..]).unwrap(); + assert_eq!(dest_state_after.amount, 400); +} From 29e516dcc0a2573b8eb8eff01f85e83c45e9f8cf Mon Sep 17 00:00:00 2001 From: ananas Date: Fri, 20 Feb 2026 21:21:58 +0000 Subject: [PATCH 21/26] fmt --- sdk-tests/sdk-light-token-pinocchio/src/lib.rs | 12 ++++++------ .../tests/test_approve_revoke.rs | 14 +++++++------- .../tests/test_ctoken_mint_to.rs | 10 +++++----- sdk-tests/sdk-light-token-test/src/lib.rs | 8 +++----- .../tests/test_ctoken_mint_to.rs | 10 +++++----- 5 files changed, 26 insertions(+), 28 deletions(-) diff --git a/sdk-tests/sdk-light-token-pinocchio/src/lib.rs b/sdk-tests/sdk-light-token-pinocchio/src/lib.rs index 1f1aed05cb..054b4da2fa 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/lib.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/lib.rs @@ -20,10 +20,12 @@ mod transfer_spl_ctoken; // Re-export all instruction data types pub use approve::{ - process_approve_invoke, process_approve_invoke_signed, - process_approve_invoke_with_fee_payer, ApproveData, + process_approve_invoke, process_approve_invoke_signed, process_approve_invoke_with_fee_payer, + ApproveData, +}; +pub use burn::{ + process_burn_invoke, process_burn_invoke_signed, process_burn_invoke_with_fee_payer, BurnData, }; -pub use burn::{process_burn_invoke, process_burn_invoke_signed, process_burn_invoke_with_fee_payer, BurnData}; pub use close::{process_close_account_invoke, process_close_account_invoke_signed}; pub use create_ata::{process_create_ata_invoke, process_create_ata_invoke_signed, CreateAtaData}; pub use create_mint::{ @@ -367,9 +369,7 @@ pub fn process_instruction( .map_err(|_| ProgramError::InvalidInstructionData)?; process_approve_invoke_with_fee_payer(accounts, data) } - InstructionType::RevokeInvokeWithFeePayer => { - process_revoke_invoke_with_fee_payer(accounts) - } + InstructionType::RevokeInvokeWithFeePayer => process_revoke_invoke_with_fee_payer(accounts), _ => Err(ProgramError::InvalidInstructionData), } } diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_approve_revoke.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_approve_revoke.rs index 97ceef7e4d..8ce300f455 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/test_approve_revoke.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_approve_revoke.rs @@ -114,10 +114,10 @@ async fn test_approve_invoke_signed() { accounts: vec![ AccountMeta::new(ata, false), // token_account AccountMeta::new_readonly(delegate.pubkey(), false), // delegate - AccountMeta::new_readonly(pda_owner, false), // PDA owner (program signs, readonly) - AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(pda_owner, false), // PDA owner (program signs, readonly) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program AccountMeta::new_readonly(light_token_program, false), // light_token_program - AccountMeta::new(payer.pubkey(), true), // fee_payer + AccountMeta::new(payer.pubkey(), true), // fee_payer ], data: instruction_data, }; @@ -269,7 +269,7 @@ async fn test_revoke_invoke_signed() { accounts: vec![ AccountMeta::new(ata, false), AccountMeta::new_readonly(delegate.pubkey(), false), - AccountMeta::new_readonly(pda_owner, false), // PDA owner (program signs, readonly) + AccountMeta::new_readonly(pda_owner, false), // PDA owner (program signs, readonly) AccountMeta::new_readonly(Pubkey::default(), false), AccountMeta::new_readonly(light_token_program, false), AccountMeta::new(payer.pubkey(), true), // fee_payer @@ -297,10 +297,10 @@ async fn test_revoke_invoke_signed() { program_id: PROGRAM_ID, accounts: vec![ AccountMeta::new(ata, false), // token_account - AccountMeta::new_readonly(pda_owner, false), // PDA owner (program signs, readonly) - AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(pda_owner, false), // PDA owner (program signs, readonly) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program AccountMeta::new_readonly(light_token_program, false), // light_token_program - AccountMeta::new(payer.pubkey(), true), // fee_payer + AccountMeta::new(payer.pubkey(), true), // fee_payer ], data: revoke_instruction_data, }; diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs index 33ab42bdc0..89cee4790c 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs @@ -132,12 +132,12 @@ async fn test_ctoken_mint_to_invoke_with_separate_fee_payer() { let instruction = Instruction { program_id: PROGRAM_ID, accounts: vec![ - AccountMeta::new(mint_pda, false), // mint - AccountMeta::new(ata, false), // destination - AccountMeta::new_readonly(payer.pubkey(), true), // authority (readonly signer) - AccountMeta::new_readonly(system_program, false), // system_program + AccountMeta::new(mint_pda, false), // mint + AccountMeta::new(ata, false), // destination + AccountMeta::new_readonly(payer.pubkey(), true), // authority (readonly signer) + AccountMeta::new_readonly(system_program, false), // system_program AccountMeta::new_readonly(light_token_program, false), // light_token_program - AccountMeta::new(fee_payer_keypair.pubkey(), true), // fee_payer (separate) + AccountMeta::new(fee_payer_keypair.pubkey(), true), // fee_payer (separate) ], data: instruction_data, }; diff --git a/sdk-tests/sdk-light-token-test/src/lib.rs b/sdk-tests/sdk-light-token-test/src/lib.rs index 50ca5281a7..b3404f4e41 100644 --- a/sdk-tests/sdk-light-token-test/src/lib.rs +++ b/sdk-tests/sdk-light-token-test/src/lib.rs @@ -48,8 +48,8 @@ use solana_program::{ }; pub use thaw::{process_thaw_invoke, process_thaw_invoke_signed}; pub use transfer::{ - process_transfer_invoke, process_transfer_invoke_signed, process_transfer_invoke_with_fee_payer, - TransferData, + process_transfer_invoke, process_transfer_invoke_signed, + process_transfer_invoke_with_fee_payer, TransferData, }; pub use transfer_checked::{ process_transfer_checked_invoke, process_transfer_checked_invoke_signed, TransferCheckedData, @@ -375,9 +375,7 @@ pub fn process_instruction( .map_err(|_| ProgramError::InvalidInstructionData)?; process_approve_invoke_with_fee_payer(accounts, data) } - InstructionType::RevokeInvokeWithFeePayer => { - process_revoke_invoke_with_fee_payer(accounts) - } + InstructionType::RevokeInvokeWithFeePayer => process_revoke_invoke_with_fee_payer(accounts), _ => Err(ProgramError::InvalidInstructionData), } } diff --git a/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs b/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs index 5bd1a70339..e17a131ff1 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs @@ -293,12 +293,12 @@ async fn test_ctoken_mint_to_invoke_with_separate_fee_payer() { let instruction = Instruction { program_id: ID, accounts: vec![ - AccountMeta::new(mint_pda, false), // mint - AccountMeta::new(ata, false), // destination + AccountMeta::new(mint_pda, false), // mint + AccountMeta::new(ata, false), // destination AccountMeta::new_readonly(payer.pubkey(), true), // authority (signer, not fee_payer) - AccountMeta::new_readonly(system_program, false), // system_program - AccountMeta::new_readonly(light_token_program, false), // light_token_program - AccountMeta::new(fee_payer_keypair.pubkey(), true), // fee_payer + AccountMeta::new_readonly(system_program, false), // system_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new(fee_payer_keypair.pubkey(), true), // fee_payer ], data: instruction_data, }; From eab19416198aa94f4629b61b39b1bc94c2ec2d4b Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Tue, 24 Feb 2026 19:02:23 +0000 Subject: [PATCH 22/26] align change --- sdk-libs/token-pinocchio/src/instruction/approve.rs | 2 +- sdk-libs/token-pinocchio/src/instruction/burn.rs | 2 +- sdk-libs/token-pinocchio/src/instruction/burn_checked.rs | 2 +- sdk-libs/token-pinocchio/src/instruction/mint_to.rs | 2 +- sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs | 2 +- sdk-libs/token-pinocchio/src/instruction/revoke.rs | 2 +- sdk-libs/token-pinocchio/src/instruction/transfer.rs | 2 +- .../token-pinocchio/src/instruction/transfer_checked.rs | 6 ++---- sdk-libs/token-sdk/src/instruction/approve.rs | 4 ++-- sdk-libs/token-sdk/src/instruction/burn.rs | 4 ++-- sdk-libs/token-sdk/src/instruction/burn_checked.rs | 4 ++-- sdk-libs/token-sdk/src/instruction/mint_to.rs | 4 ++-- sdk-libs/token-sdk/src/instruction/mint_to_checked.rs | 4 ++-- sdk-libs/token-sdk/src/instruction/revoke.rs | 4 ++-- sdk-libs/token-sdk/src/instruction/transfer.rs | 4 ++-- sdk-libs/token-sdk/src/instruction/transfer_checked.rs | 4 ++-- sdk-tests/sdk-light-token-test/src/transfer_checked.rs | 5 +++-- .../sdk-light-token-test/tests/test_transfer_checked.rs | 3 +++ 18 files changed, 31 insertions(+), 29 deletions(-) diff --git a/sdk-libs/token-pinocchio/src/instruction/approve.rs b/sdk-libs/token-pinocchio/src/instruction/approve.rs index 430ba8cea1..9639c08c86 100644 --- a/sdk-libs/token-pinocchio/src/instruction/approve.rs +++ b/sdk-libs/token-pinocchio/src/instruction/approve.rs @@ -33,7 +33,7 @@ pub struct ApproveCpi<'info> { pub owner: &'info AccountInfo, pub system_program: &'info AccountInfo, pub amount: u64, - /// Fee payer for compressible rent top-ups (writable signer) + /// Fee payer for rent top-ups. pub fee_payer: &'info AccountInfo, } diff --git a/sdk-libs/token-pinocchio/src/instruction/burn.rs b/sdk-libs/token-pinocchio/src/instruction/burn.rs index 789cbf1a5d..7c0ba5103b 100644 --- a/sdk-libs/token-pinocchio/src/instruction/burn.rs +++ b/sdk-libs/token-pinocchio/src/instruction/burn.rs @@ -33,7 +33,7 @@ pub struct BurnCpi<'info> { pub amount: u64, pub authority: &'info AccountInfo, pub system_program: &'info AccountInfo, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: &'info AccountInfo, } diff --git a/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs b/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs index 7253975341..52dc8f8817 100644 --- a/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs +++ b/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs @@ -35,7 +35,7 @@ pub struct BurnCheckedCpi<'info> { pub decimals: u8, pub authority: &'info AccountInfo, pub system_program: &'info AccountInfo, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: &'info AccountInfo, } diff --git a/sdk-libs/token-pinocchio/src/instruction/mint_to.rs b/sdk-libs/token-pinocchio/src/instruction/mint_to.rs index 978033f73a..780aa496e5 100644 --- a/sdk-libs/token-pinocchio/src/instruction/mint_to.rs +++ b/sdk-libs/token-pinocchio/src/instruction/mint_to.rs @@ -33,7 +33,7 @@ pub struct MintToCpi<'info> { pub amount: u64, pub authority: &'info AccountInfo, pub system_program: &'info AccountInfo, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: &'info AccountInfo, } diff --git a/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs b/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs index 60602e2f1c..a7d07f0c2f 100644 --- a/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs +++ b/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs @@ -35,7 +35,7 @@ pub struct MintToCheckedCpi<'info> { pub decimals: u8, pub authority: &'info AccountInfo, pub system_program: &'info AccountInfo, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: &'info AccountInfo, } diff --git a/sdk-libs/token-pinocchio/src/instruction/revoke.rs b/sdk-libs/token-pinocchio/src/instruction/revoke.rs index 6c53784e7a..962a0f178a 100644 --- a/sdk-libs/token-pinocchio/src/instruction/revoke.rs +++ b/sdk-libs/token-pinocchio/src/instruction/revoke.rs @@ -29,7 +29,7 @@ pub struct RevokeCpi<'info> { pub token_account: &'info AccountInfo, pub owner: &'info AccountInfo, pub system_program: &'info AccountInfo, - /// Fee payer for compressible rent top-ups (writable signer) + /// Fee payer for rent top-ups. pub fee_payer: &'info AccountInfo, } diff --git a/sdk-libs/token-pinocchio/src/instruction/transfer.rs b/sdk-libs/token-pinocchio/src/instruction/transfer.rs index 62c41f5f18..223d16710b 100644 --- a/sdk-libs/token-pinocchio/src/instruction/transfer.rs +++ b/sdk-libs/token-pinocchio/src/instruction/transfer.rs @@ -33,7 +33,7 @@ pub struct TransferCpi<'info> { pub amount: u64, pub authority: &'info AccountInfo, pub system_program: &'info AccountInfo, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: &'info AccountInfo, } diff --git a/sdk-libs/token-pinocchio/src/instruction/transfer_checked.rs b/sdk-libs/token-pinocchio/src/instruction/transfer_checked.rs index aceebe5a93..be7baac4e2 100644 --- a/sdk-libs/token-pinocchio/src/instruction/transfer_checked.rs +++ b/sdk-libs/token-pinocchio/src/instruction/transfer_checked.rs @@ -10,8 +10,6 @@ use pinocchio::{ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; -const SYSTEM_PROGRAM_ID: Pubkey = [0u8; 32]; - /// Transfer ctoken checked via CPI. /// /// # Example @@ -39,7 +37,7 @@ pub struct TransferCheckedCpi<'info> { pub decimals: u8, pub authority: &'info AccountInfo, pub system_program: &'info AccountInfo, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: &'info AccountInfo, } @@ -61,7 +59,7 @@ impl<'info> TransferCheckedCpi<'info> { AccountMeta::readonly(self.mint.key()), AccountMeta::writable(self.destination.key()), AccountMeta::readonly_signer(self.authority.key()), - AccountMeta::readonly(&SYSTEM_PROGRAM_ID), + AccountMeta::readonly(self.system_program.key()), AccountMeta::writable_signer(self.fee_payer.key()), ]; diff --git a/sdk-libs/token-sdk/src/instruction/approve.rs b/sdk-libs/token-sdk/src/instruction/approve.rs index a7de2d4dae..7651554177 100644 --- a/sdk-libs/token-sdk/src/instruction/approve.rs +++ b/sdk-libs/token-sdk/src/instruction/approve.rs @@ -31,7 +31,7 @@ pub struct Approve { pub owner: Pubkey, /// Amount of tokens to delegate pub amount: u64, - /// Fee payer for compressible rent top-ups (writable signer) + /// Fee payer for rent top-ups. pub fee_payer: Pubkey, } @@ -61,7 +61,7 @@ pub struct ApproveCpi<'info> { pub owner: AccountInfo<'info>, pub system_program: AccountInfo<'info>, pub amount: u64, - /// Fee payer for compressible rent top-ups (writable signer) + /// Fee payer for rent top-ups. pub fee_payer: AccountInfo<'info>, } diff --git a/sdk-libs/token-sdk/src/instruction/burn.rs b/sdk-libs/token-sdk/src/instruction/burn.rs index 257e59420b..8819f18cae 100644 --- a/sdk-libs/token-sdk/src/instruction/burn.rs +++ b/sdk-libs/token-sdk/src/instruction/burn.rs @@ -31,7 +31,7 @@ pub struct Burn { pub amount: u64, /// Owner of the Light Token account pub authority: Pubkey, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: Pubkey, } @@ -61,7 +61,7 @@ pub struct BurnCpi<'info> { pub amount: u64, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: AccountInfo<'info>, } diff --git a/sdk-libs/token-sdk/src/instruction/burn_checked.rs b/sdk-libs/token-sdk/src/instruction/burn_checked.rs index b3e2e4ff1c..edd32dcacc 100644 --- a/sdk-libs/token-sdk/src/instruction/burn_checked.rs +++ b/sdk-libs/token-sdk/src/instruction/burn_checked.rs @@ -34,7 +34,7 @@ pub struct BurnChecked { pub decimals: u8, /// Owner of the Light Token account pub authority: Pubkey, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: Pubkey, } @@ -66,7 +66,7 @@ pub struct BurnCheckedCpi<'info> { pub decimals: u8, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: AccountInfo<'info>, } diff --git a/sdk-libs/token-sdk/src/instruction/mint_to.rs b/sdk-libs/token-sdk/src/instruction/mint_to.rs index 029564ee13..383271a861 100644 --- a/sdk-libs/token-sdk/src/instruction/mint_to.rs +++ b/sdk-libs/token-sdk/src/instruction/mint_to.rs @@ -31,7 +31,7 @@ pub struct MintTo { pub amount: u64, /// Mint authority pub authority: Pubkey, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: Pubkey, } @@ -61,7 +61,7 @@ pub struct MintToCpi<'info> { pub amount: u64, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: AccountInfo<'info>, } diff --git a/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs b/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs index 8d844ee74f..8ce6c148cb 100644 --- a/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs +++ b/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs @@ -34,7 +34,7 @@ pub struct MintToChecked { pub decimals: u8, /// Mint authority pub authority: Pubkey, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: Pubkey, } @@ -66,7 +66,7 @@ pub struct MintToCheckedCpi<'info> { pub decimals: u8, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: AccountInfo<'info>, } diff --git a/sdk-libs/token-sdk/src/instruction/revoke.rs b/sdk-libs/token-sdk/src/instruction/revoke.rs index 49df604bed..72bbfb8cf6 100644 --- a/sdk-libs/token-sdk/src/instruction/revoke.rs +++ b/sdk-libs/token-sdk/src/instruction/revoke.rs @@ -24,7 +24,7 @@ pub struct Revoke { pub token_account: Pubkey, /// Owner of the Light Token account (readonly signer) pub owner: Pubkey, - /// Fee payer for compressible rent top-ups (writable signer) + /// Fee payer for rent top-ups. pub fee_payer: Pubkey, } @@ -49,7 +49,7 @@ pub struct RevokeCpi<'info> { pub token_account: AccountInfo<'info>, pub owner: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Fee payer for compressible rent top-ups (writable signer) + /// Fee payer for rent top-ups. pub fee_payer: AccountInfo<'info>, } diff --git a/sdk-libs/token-sdk/src/instruction/transfer.rs b/sdk-libs/token-sdk/src/instruction/transfer.rs index a9cd5187af..944b824906 100644 --- a/sdk-libs/token-sdk/src/instruction/transfer.rs +++ b/sdk-libs/token-sdk/src/instruction/transfer.rs @@ -27,7 +27,7 @@ pub struct Transfer { pub destination: Pubkey, pub amount: u64, pub authority: Pubkey, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: Pubkey, } @@ -57,7 +57,7 @@ pub struct TransferCpi<'info> { pub amount: u64, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: AccountInfo<'info>, } diff --git a/sdk-libs/token-sdk/src/instruction/transfer_checked.rs b/sdk-libs/token-sdk/src/instruction/transfer_checked.rs index 0199526d6e..09df622bde 100644 --- a/sdk-libs/token-sdk/src/instruction/transfer_checked.rs +++ b/sdk-libs/token-sdk/src/instruction/transfer_checked.rs @@ -32,7 +32,7 @@ pub struct TransferChecked { pub amount: u64, pub decimals: u8, pub authority: Pubkey, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: Pubkey, } @@ -67,7 +67,7 @@ pub struct TransferCheckedCpi<'info> { pub decimals: u8, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, - /// Fee payer for rent top-ups (writable signer). Authority stays readonly. + /// Fee payer for rent top-ups. pub fee_payer: AccountInfo<'info>, } diff --git a/sdk-tests/sdk-light-token-test/src/transfer_checked.rs b/sdk-tests/sdk-light-token-test/src/transfer_checked.rs index 043c6580e0..29f58d59ce 100644 --- a/sdk-tests/sdk-light-token-test/src/transfer_checked.rs +++ b/sdk-tests/sdk-light-token-test/src/transfer_checked.rs @@ -19,11 +19,12 @@ pub struct TransferCheckedData { /// - accounts[2]: destination ctoken account /// - accounts[3]: authority (signer) /// - accounts[4]: system_program +/// - accounts[5]: fee_payer (writable, signer) pub fn process_transfer_checked_invoke( accounts: &[AccountInfo], data: TransferCheckedData, ) -> Result<(), ProgramError> { - if accounts.len() < 5 { + if accounts.len() < 6 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -35,7 +36,7 @@ pub fn process_transfer_checked_invoke( decimals: data.decimals, authority: accounts[3].clone(), system_program: accounts[4].clone(), - fee_payer: accounts[3].clone(), + fee_payer: accounts[5].clone(), } .invoke()?; diff --git a/sdk-tests/sdk-light-token-test/tests/test_transfer_checked.rs b/sdk-tests/sdk-light-token-test/tests/test_transfer_checked.rs index 9730f0eecb..9d1a8ca283 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_transfer_checked.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_transfer_checked.rs @@ -153,6 +153,7 @@ async fn test_ctoken_transfer_checked_spl_mint() { AccountMeta::new(dest_ata, false), AccountMeta::new_readonly(source_owner, true), AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(payer.pubkey(), true), // fee_payer AccountMeta::new_readonly(light_token_program, false), ], data: instruction_data, @@ -259,6 +260,7 @@ async fn test_ctoken_transfer_checked_t22_mint() { AccountMeta::new(dest_ata, false), AccountMeta::new_readonly(source_owner, true), AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(payer.pubkey(), true), // fee_payer AccountMeta::new_readonly(light_token_program, false), ], data: instruction_data, @@ -320,6 +322,7 @@ async fn test_ctoken_transfer_checked_mint() { AccountMeta::new(dest_ata, false), AccountMeta::new_readonly(source_owner, true), AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(payer.pubkey(), true), // fee_payer AccountMeta::new_readonly(light_token_program, false), ], data: instruction_data, From 86e6d009e7b387553511283370c2dd5fed481841 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Tue, 24 Feb 2026 19:58:48 +0000 Subject: [PATCH 23/26] Switch TransferInterface LightToLight from Transfer to TransferChecked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add top-level mint field to TransferInterface/TransferInterfaceCpi so LightToLight path uses TransferChecked (disc 12, validates decimals) instead of plain Transfer (disc 3). Require mint account in test handlers (min accounts 7→8) and pass it in LightToLight test callsites. --- .../src/actions/transfer_interface.rs | 1 + .../src/instruction/transfer_interface.rs | 15 ++++++++++--- .../src/instruction/transfer_interface.rs | 21 ++++++++++++++++--- sdk-libs/token-sdk/tests/transfer_type.rs | 10 +++++++++ .../src/transfer_interface.rs | 10 +++++---- .../tests/test_transfer_interface.rs | 8 ++++--- .../src/transfer_interface.rs | 10 +++++---- .../tests/test_transfer_interface.rs | 8 ++++--- 8 files changed, 63 insertions(+), 20 deletions(-) diff --git a/sdk-libs/token-client/src/actions/transfer_interface.rs b/sdk-libs/token-client/src/actions/transfer_interface.rs index 741d65d599..a76ddaeb2d 100644 --- a/sdk-libs/token-client/src/actions/transfer_interface.rs +++ b/sdk-libs/token-client/src/actions/transfer_interface.rs @@ -103,6 +103,7 @@ impl TransferInterface { decimals: self.decimals, authority: authority.pubkey(), payer: payer.pubkey(), + mint: self.mint, spl_interface, source_owner, destination_owner, diff --git a/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs b/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs index ee123d480f..a3df0ccdbf 100644 --- a/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs +++ b/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs @@ -9,7 +9,8 @@ use pinocchio::{ }; use super::{ - transfer::TransferCpi, transfer_from_spl::TransferFromSplCpi, transfer_to_spl::TransferToSplCpi, + transfer_checked::TransferCheckedCpi, transfer_from_spl::TransferFromSplCpi, + transfer_to_spl::TransferToSplCpi, }; /// SPL Token transfer_checked instruction discriminator @@ -69,6 +70,7 @@ pub struct SplInterfaceCpi<'info> { /// &authority, /// &payer, /// &compressed_token_program_authority, +/// &mint, /// &system_program, /// ) /// .invoke()?; @@ -81,6 +83,7 @@ pub struct TransferInterfaceCpi<'info> { pub authority: &'info AccountInfo, pub payer: &'info AccountInfo, pub compressed_token_program_authority: &'info AccountInfo, + pub mint: &'info AccountInfo, pub spl_interface: Option>, /// System program - required for compressible account lamport top-ups pub system_program: &'info AccountInfo, @@ -97,6 +100,7 @@ impl<'info> TransferInterfaceCpi<'info> { authority: &'info AccountInfo, payer: &'info AccountInfo, compressed_token_program_authority: &'info AccountInfo, + mint: &'info AccountInfo, system_program: &'info AccountInfo, ) -> Self { Self { @@ -107,6 +111,7 @@ impl<'info> TransferInterfaceCpi<'info> { authority, payer, compressed_token_program_authority, + mint, spl_interface: None, system_program, } @@ -126,10 +131,12 @@ impl<'info> TransferInterfaceCpi<'info> { ); match transfer_type { - TransferType::LightToLight => TransferCpi { + TransferType::LightToLight => TransferCheckedCpi { source: self.source_account, + mint: self.mint, destination: self.destination_account, amount: self.amount, + decimals: self.decimals, authority: self.authority, system_program: self.system_program, fee_payer: self.payer, @@ -222,10 +229,12 @@ impl<'info> TransferInterfaceCpi<'info> { ); match transfer_type { - TransferType::LightToLight => TransferCpi { + TransferType::LightToLight => TransferCheckedCpi { source: self.source_account, + mint: self.mint, destination: self.destination_account, amount: self.amount, + decimals: self.decimals, authority: self.authority, system_program: self.system_program, fee_payer: self.payer, diff --git a/sdk-libs/token-sdk/src/instruction/transfer_interface.rs b/sdk-libs/token-sdk/src/instruction/transfer_interface.rs index 218c3febd3..bb160193db 100644 --- a/sdk-libs/token-sdk/src/instruction/transfer_interface.rs +++ b/sdk-libs/token-sdk/src/instruction/transfer_interface.rs @@ -5,7 +5,8 @@ use solana_program_error::ProgramError; use solana_pubkey::Pubkey; use super::{ - transfer::Transfer, transfer_from_spl::TransferFromSpl, transfer_to_spl::TransferToSpl, + transfer_checked::TransferChecked, transfer_from_spl::TransferFromSpl, + transfer_to_spl::TransferToSpl, }; use crate::error::LightTokenError; @@ -93,6 +94,7 @@ pub struct SplInterfaceCpi<'info> { /// # let destination = Pubkey::new_unique(); /// # let authority = Pubkey::new_unique(); /// # let payer = Pubkey::new_unique(); +/// # let mint = Pubkey::new_unique(); /// // For light -> light transfer (source_owner and destination_owner are LIGHT_TOKEN_PROGRAM_ID) /// let instruction = TransferInterface { /// source, @@ -101,6 +103,7 @@ pub struct SplInterfaceCpi<'info> { /// decimals: 9, /// authority, /// payer, +/// mint, /// spl_interface: None, /// source_owner: LIGHT_TOKEN_PROGRAM_ID, /// destination_owner: LIGHT_TOKEN_PROGRAM_ID, @@ -114,6 +117,7 @@ pub struct TransferInterface { pub decimals: u8, pub authority: Pubkey, pub payer: Pubkey, + pub mint: Pubkey, pub spl_interface: Option, /// Owner of the source account (used to determine transfer type) pub source_owner: Pubkey, @@ -125,10 +129,12 @@ impl TransferInterface { /// Build instruction based on detected transfer type pub fn instruction(self) -> Result { match determine_transfer_type(&self.source_owner, &self.destination_owner)? { - TransferType::LightToLight => Transfer { + TransferType::LightToLight => TransferChecked { source: self.source, + mint: self.mint, destination: self.destination, amount: self.amount, + decimals: self.decimals, authority: self.authority, fee_payer: self.payer, } @@ -207,6 +213,7 @@ impl<'info> From<&TransferInterfaceCpi<'info>> for TransferInterface { decimals: cpi.decimals, authority: *cpi.authority.key, payer: *cpi.payer.key, + mint: *cpi.mint.key, spl_interface: cpi.spl_interface.as_ref().map(SplInterface::from), source_owner: *cpi.source_account.owner, destination_owner: *cpi.destination_account.owner, @@ -223,6 +230,7 @@ impl<'info> From<&TransferInterfaceCpi<'info>> for TransferInterface { /// # let authority: AccountInfo = todo!(); /// # let payer: AccountInfo = todo!(); /// # let compressed_token_program_authority: AccountInfo = todo!(); +/// # let mint: AccountInfo = todo!(); /// # let system_program: AccountInfo = todo!(); /// TransferInterfaceCpi::new( /// 100, // amount @@ -232,6 +240,7 @@ impl<'info> From<&TransferInterfaceCpi<'info>> for TransferInterface { /// authority, /// payer, /// compressed_token_program_authority, +/// mint, /// system_program, /// ) /// .invoke()?; @@ -245,6 +254,7 @@ pub struct TransferInterfaceCpi<'info> { pub authority: AccountInfo<'info>, pub payer: AccountInfo<'info>, pub compressed_token_program_authority: AccountInfo<'info>, + pub mint: AccountInfo<'info>, pub spl_interface: Option>, /// System program - required for compressible account lamport top-ups pub system_program: AccountInfo<'info>, @@ -253,12 +263,13 @@ pub struct TransferInterfaceCpi<'info> { impl<'info> TransferInterfaceCpi<'info> { /// # Arguments /// * `amount` - Amount to transfer - /// * `decimals` - Token decimals (required for SPL transfers) + /// * `decimals` - Token decimals /// * `source_account` - Source token account (can be light or SPL) /// * `destination_account` - Destination token account (can be light or SPL) /// * `authority` - Authority for the transfer (must be signer) /// * `payer` - Payer for the transaction /// * `compressed_token_program_authority` - Light Token program authority + /// * `mint` - Token mint account /// * `system_program` - System program (required for compressible account lamport top-ups) #[allow(clippy::too_many_arguments)] pub fn new( @@ -269,6 +280,7 @@ impl<'info> TransferInterfaceCpi<'info> { authority: AccountInfo<'info>, payer: AccountInfo<'info>, compressed_token_program_authority: AccountInfo<'info>, + mint: AccountInfo<'info>, system_program: AccountInfo<'info>, ) -> Self { Self { @@ -279,6 +291,7 @@ impl<'info> TransferInterfaceCpi<'info> { decimals, payer, compressed_token_program_authority, + mint, spl_interface: None, system_program, } @@ -337,6 +350,7 @@ impl<'info> TransferInterfaceCpi<'info> { TransferType::LightToLight => { let account_infos = [ self.source_account, + self.mint, self.destination_account, self.authority, self.system_program, @@ -409,6 +423,7 @@ impl<'info> TransferInterfaceCpi<'info> { TransferType::LightToLight => { let account_infos = [ self.source_account, + self.mint, self.destination_account, self.authority, self.system_program, diff --git a/sdk-libs/token-sdk/tests/transfer_type.rs b/sdk-libs/token-sdk/tests/transfer_type.rs index 2e1a67f1df..c8d22af15e 100644 --- a/sdk-libs/token-sdk/tests/transfer_type.rs +++ b/sdk-libs/token-sdk/tests/transfer_type.rs @@ -102,6 +102,7 @@ fn test_transfer_interface_light_to_light_no_spl_interface() { let destination = Pubkey::new_unique(); let authority = Pubkey::new_unique(); let payer = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); // Create TransferInterface for light-to-light transfer let transfer = TransferInterface { @@ -111,6 +112,7 @@ fn test_transfer_interface_light_to_light_no_spl_interface() { decimals: 9, authority, payer, + mint, spl_interface: None, // No SPL interface needed source_owner: LIGHT_TOKEN_PROGRAM_ID, @@ -137,6 +139,7 @@ fn test_transfer_interface_light_to_spl_requires_interface() { let destination = Pubkey::new_unique(); let authority = Pubkey::new_unique(); let payer = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); // Create TransferInterface for light-to-SPL transfer without interface let transfer = TransferInterface { @@ -146,6 +149,7 @@ fn test_transfer_interface_light_to_spl_requires_interface() { decimals: 9, authority, payer, + mint, spl_interface: None, // Missing required interface source_owner: LIGHT_TOKEN_PROGRAM_ID, @@ -167,6 +171,7 @@ fn test_transfer_interface_spl_to_light_requires_interface() { let destination = Pubkey::new_unique(); let authority = Pubkey::new_unique(); let payer = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); // Create TransferInterface for SPL-to-light transfer without interface let transfer = TransferInterface { @@ -176,6 +181,7 @@ fn test_transfer_interface_spl_to_light_requires_interface() { decimals: 9, authority, payer, + mint, spl_interface: None, // Missing required interface source_owner: SPL_TOKEN_PROGRAM_ID, @@ -207,6 +213,7 @@ fn test_transfer_interface_light_to_spl_with_interface() { decimals: 9, authority, payer, + mint, spl_interface: Some(SplInterface { mint, spl_token_program: SPL_TOKEN_PROGRAM_ID, @@ -233,6 +240,7 @@ fn test_transfer_interface_spl_to_spl_requires_interface() { let destination = Pubkey::new_unique(); let authority = Pubkey::new_unique(); let payer = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); // Both owners are the same SPL token program let transfer = TransferInterface { @@ -242,6 +250,7 @@ fn test_transfer_interface_spl_to_spl_requires_interface() { decimals: 9, authority, payer, + mint, spl_interface: None, // Missing interface source_owner: SPL_TOKEN_PROGRAM_ID, @@ -274,6 +283,7 @@ fn test_transfer_interface_spl_program_mismatch() { decimals: 9, authority, payer, + mint, spl_interface: Some(SplInterface { mint, spl_token_program: SPL_TOKEN_PROGRAM_ID, diff --git a/sdk-tests/sdk-light-token-pinocchio/src/transfer_interface.rs b/sdk-tests/sdk-light-token-pinocchio/src/transfer_interface.rs index 29895480ed..9ee2e4bf93 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/transfer_interface.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/transfer_interface.rs @@ -35,15 +35,15 @@ pub struct TransferInterfaceData { /// - accounts[4]: payer (signer) /// - accounts[5]: compressed_token_program_authority /// - accounts[6]: system_program -/// For SPL bridge (optional, required for SPL<->Light Token): /// - accounts[7]: mint +/// For SPL bridge (optional, required for SPL<->Light Token): /// - accounts[8]: spl_interface_pda /// - accounts[9]: spl_token_program pub fn process_transfer_interface_invoke( accounts: &[AccountInfo], data: TransferInterfaceData, ) -> Result<(), ProgramError> { - if accounts.len() < 7 { + if accounts.len() < 8 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -55,6 +55,7 @@ pub fn process_transfer_interface_invoke( &accounts[3], // authority &accounts[4], // payer &accounts[5], // compressed_token_program_authority + &accounts[7], // mint &accounts[6], // system_program ); @@ -85,15 +86,15 @@ pub fn process_transfer_interface_invoke( /// - accounts[4]: payer (signer) /// - accounts[5]: compressed_token_program_authority /// - accounts[6]: system_program -/// For SPL bridge (optional, required for SPL<->Light Token): /// - accounts[7]: mint +/// For SPL bridge (optional, required for SPL<->Light Token): /// - accounts[8]: spl_interface_pda /// - accounts[9]: spl_token_program pub fn process_transfer_interface_invoke_signed( accounts: &[AccountInfo], data: TransferInterfaceData, ) -> Result<(), ProgramError> { - if accounts.len() < 7 { + if accounts.len() < 8 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -114,6 +115,7 @@ pub fn process_transfer_interface_invoke_signed( &accounts[3], // authority (PDA) &accounts[4], // payer &accounts[5], // compressed_token_program_authority + &accounts[7], // mint &accounts[6], // system_program ); diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_interface.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_interface.rs index 3a5d82b608..4b28893457 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_interface.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_interface.rs @@ -376,7 +376,7 @@ async fn test_transfer_interface_ctoken_to_ctoken_invoke() { }; let wrapper_instruction_data = [vec![19u8], data.try_to_vec().unwrap()].concat(); - // For Light Token->Light Token, we need 7 accounts (no SPL bridge, but system_program is required) + // For Light Token->Light Token, we need 8 accounts (mint required for TransferChecked) let wrapper_accounts = vec![ AccountMeta::new_readonly(compressed_token_program_id, false), AccountMeta::new(sender_ctoken, false), // source (Light Token) @@ -385,6 +385,7 @@ async fn test_transfer_interface_ctoken_to_ctoken_invoke() { AccountMeta::new(payer.pubkey(), true), // payer AccountMeta::new_readonly(cpi_authority_pda, false), AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(mint, false), // mint ]; let instruction = Instruction { @@ -807,7 +808,7 @@ async fn test_transfer_interface_ctoken_to_ctoken_invoke_signed() { // Discriminator 20 = TransferInterfaceInvokeSigned let wrapper_instruction_data = [vec![20u8], data.try_to_vec().unwrap()].concat(); - // For Light Token->Light Token, we only need 6 accounts (no SPL bridge) + // For Light Token->Light Token, we need 8 accounts (mint required for TransferChecked) let wrapper_accounts = vec![ AccountMeta::new_readonly(compressed_token_program_id, false), AccountMeta::new(source_ctoken, false), // source (Light Token owned by PDA) @@ -815,7 +816,8 @@ async fn test_transfer_interface_ctoken_to_ctoken_invoke_signed() { AccountMeta::new(authority_pda, false), // authority (PDA, writable, program signs) AccountMeta::new(payer.pubkey(), true), // payer AccountMeta::new_readonly(cpi_authority_pda, false), - AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(mint, false), // mint ]; let instruction = Instruction { diff --git a/sdk-tests/sdk-light-token-test/src/transfer_interface.rs b/sdk-tests/sdk-light-token-test/src/transfer_interface.rs index 0a60819d17..046ccb9043 100644 --- a/sdk-tests/sdk-light-token-test/src/transfer_interface.rs +++ b/sdk-tests/sdk-light-token-test/src/transfer_interface.rs @@ -31,15 +31,15 @@ pub struct TransferInterfaceData { /// - accounts[4]: payer (signer) /// - accounts[5]: compressed_token_program_authority /// - accounts[6]: system_program -/// For SPL bridge (optional, required for SPL<->Light Token): /// - accounts[7]: mint +/// For SPL bridge (optional, required for SPL<->Light Token): /// - accounts[8]: spl_interface_pda /// - accounts[9]: spl_token_program pub fn process_transfer_interface_invoke( accounts: &[AccountInfo], data: TransferInterfaceData, ) -> Result<(), ProgramError> { - if accounts.len() < 7 { + if accounts.len() < 8 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -51,6 +51,7 @@ pub fn process_transfer_interface_invoke( accounts[3].clone(), // authority accounts[4].clone(), // payer accounts[5].clone(), // compressed_token_program_authority + accounts[7].clone(), // mint accounts[6].clone(), // system_program ); @@ -81,15 +82,15 @@ pub fn process_transfer_interface_invoke( /// - accounts[4]: payer (signer) /// - accounts[5]: compressed_token_program_authority /// - accounts[6]: system_program -/// For SPL bridge (optional, required for SPL<->Light Token): /// - accounts[7]: mint +/// For SPL bridge (optional, required for SPL<->Light Token): /// - accounts[8]: spl_interface_pda /// - accounts[9]: spl_token_program pub fn process_transfer_interface_invoke_signed( accounts: &[AccountInfo], data: TransferInterfaceData, ) -> Result<(), ProgramError> { - if accounts.len() < 7 { + if accounts.len() < 8 { return Err(ProgramError::NotEnoughAccountKeys); } @@ -110,6 +111,7 @@ pub fn process_transfer_interface_invoke_signed( accounts[3].clone(), // authority (PDA) accounts[4].clone(), // payer accounts[5].clone(), // compressed_token_program_authority + accounts[7].clone(), // mint accounts[6].clone(), // system_program ); diff --git a/sdk-tests/sdk-light-token-test/tests/test_transfer_interface.rs b/sdk-tests/sdk-light-token-test/tests/test_transfer_interface.rs index 7347ddf7f7..7eea5bcf02 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_transfer_interface.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_transfer_interface.rs @@ -375,7 +375,7 @@ async fn test_transfer_interface_ctoken_to_ctoken_invoke() { }; let wrapper_instruction_data = [vec![19u8], data.try_to_vec().unwrap()].concat(); - // For Light Token->Light Token, we need 7 accounts (no SPL bridge, but system_program is required) + // For Light Token->Light Token, we need 8 accounts (mint required for TransferChecked) let wrapper_accounts = vec![ AccountMeta::new_readonly(compressed_token_program_id, false), AccountMeta::new(sender_ctoken, false), // source (Light Token) @@ -384,6 +384,7 @@ async fn test_transfer_interface_ctoken_to_ctoken_invoke() { AccountMeta::new(payer.pubkey(), true), // payer AccountMeta::new_readonly(cpi_authority_pda, false), AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(mint, false), // mint ]; let instruction = Instruction { @@ -806,7 +807,7 @@ async fn test_transfer_interface_ctoken_to_ctoken_invoke_signed() { // Discriminator 20 = TransferInterfaceInvokeSigned let wrapper_instruction_data = [vec![20u8], data.try_to_vec().unwrap()].concat(); - // For Light Token->Light Token, we only need 6 accounts (no SPL bridge) + // For Light Token->Light Token, we need 8 accounts (mint required for TransferChecked) let wrapper_accounts = vec![ AccountMeta::new_readonly(compressed_token_program_id, false), AccountMeta::new(source_ctoken, false), // source (Light Token owned by PDA) @@ -814,7 +815,8 @@ async fn test_transfer_interface_ctoken_to_ctoken_invoke_signed() { AccountMeta::new_readonly(authority_pda, false), // authority (PDA) AccountMeta::new(payer.pubkey(), true), // payer AccountMeta::new_readonly(cpi_authority_pda, false), - AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(mint, false), // mint ]; let instruction = Instruction { From 7d29f84c11bcd305af14d3c6fd307d2f0a0452e0 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Tue, 24 Feb 2026 22:41:52 +0000 Subject: [PATCH 24/26] add macro --- sdk-libs/token-sdk/src/instruction/burn.rs | 24 ++--------------- .../token-sdk/src/instruction/burn_checked.rs | 24 ++--------------- sdk-libs/token-sdk/src/instruction/macros.rs | 27 +++++++++++++++++++ sdk-libs/token-sdk/src/instruction/mint_to.rs | 24 ++--------------- .../src/instruction/mint_to_checked.rs | 24 ++--------------- sdk-libs/token-sdk/src/instruction/mod.rs | 3 +++ .../token-sdk/src/instruction/transfer.rs | 24 ++--------------- .../src/instruction/transfer_checked.rs | 24 ++--------------- 8 files changed, 42 insertions(+), 132 deletions(-) create mode 100644 sdk-libs/token-sdk/src/instruction/macros.rs diff --git a/sdk-libs/token-sdk/src/instruction/burn.rs b/sdk-libs/token-sdk/src/instruction/burn.rs index 8819f18cae..f65287fe4f 100644 --- a/sdk-libs/token-sdk/src/instruction/burn.rs +++ b/sdk-libs/token-sdk/src/instruction/burn.rs @@ -107,18 +107,9 @@ impl<'info> From<&BurnCpi<'info>> for Burn { } } -impl Burn { - pub fn with_max_top_up(self, max_top_up: u16) -> BurnWithTopUp { - BurnWithTopUp { - inner: self, - max_top_up, - } - } - - pub fn instruction(self) -> Result { - self.build_instruction(None) - } +impl_with_top_up!(Burn, BurnWithTopUp); +impl Burn { fn build_instruction(self, max_top_up: Option) -> Result { let accounts = vec![ AccountMeta::new(self.source, false), @@ -141,14 +132,3 @@ impl Burn { }) } } - -pub struct BurnWithTopUp { - inner: Burn, - max_top_up: u16, -} - -impl BurnWithTopUp { - pub fn instruction(self) -> Result { - self.inner.build_instruction(Some(self.max_top_up)) - } -} diff --git a/sdk-libs/token-sdk/src/instruction/burn_checked.rs b/sdk-libs/token-sdk/src/instruction/burn_checked.rs index edd32dcacc..bab9f0f2ab 100644 --- a/sdk-libs/token-sdk/src/instruction/burn_checked.rs +++ b/sdk-libs/token-sdk/src/instruction/burn_checked.rs @@ -113,18 +113,9 @@ impl<'info> From<&BurnCheckedCpi<'info>> for BurnChecked { } } -impl BurnChecked { - pub fn with_max_top_up(self, max_top_up: u16) -> BurnCheckedWithTopUp { - BurnCheckedWithTopUp { - inner: self, - max_top_up, - } - } - - pub fn instruction(self) -> Result { - self.build_instruction(None) - } +impl_with_top_up!(BurnChecked, BurnCheckedWithTopUp); +impl BurnChecked { fn build_instruction(self, max_top_up: Option) -> Result { let accounts = vec![ AccountMeta::new(self.source, false), @@ -148,14 +139,3 @@ impl BurnChecked { }) } } - -pub struct BurnCheckedWithTopUp { - inner: BurnChecked, - max_top_up: u16, -} - -impl BurnCheckedWithTopUp { - pub fn instruction(self) -> Result { - self.inner.build_instruction(Some(self.max_top_up)) - } -} diff --git a/sdk-libs/token-sdk/src/instruction/macros.rs b/sdk-libs/token-sdk/src/instruction/macros.rs new file mode 100644 index 0000000000..f28538e58f --- /dev/null +++ b/sdk-libs/token-sdk/src/instruction/macros.rs @@ -0,0 +1,27 @@ +macro_rules! impl_with_top_up { + ($base:ident, $with_top_up:ident) => { + impl $base { + pub fn with_max_top_up(self, max_top_up: u16) -> $with_top_up { + $with_top_up { + inner: self, + max_top_up, + } + } + + pub fn instruction(self) -> Result { + self.build_instruction(None) + } + } + + pub struct $with_top_up { + inner: $base, + max_top_up: u16, + } + + impl $with_top_up { + pub fn instruction(self) -> Result { + self.inner.build_instruction(Some(self.max_top_up)) + } + } + }; +} diff --git a/sdk-libs/token-sdk/src/instruction/mint_to.rs b/sdk-libs/token-sdk/src/instruction/mint_to.rs index 383271a861..4762d0abc6 100644 --- a/sdk-libs/token-sdk/src/instruction/mint_to.rs +++ b/sdk-libs/token-sdk/src/instruction/mint_to.rs @@ -107,18 +107,9 @@ impl<'info> From<&MintToCpi<'info>> for MintTo { } } -impl MintTo { - pub fn with_max_top_up(self, max_top_up: u16) -> MintToWithTopUp { - MintToWithTopUp { - inner: self, - max_top_up, - } - } - - pub fn instruction(self) -> Result { - self.build_instruction(None) - } +impl_with_top_up!(MintTo, MintToWithTopUp); +impl MintTo { fn build_instruction(self, max_top_up: Option) -> Result { let accounts = vec![ AccountMeta::new(self.mint, false), @@ -141,14 +132,3 @@ impl MintTo { }) } } - -pub struct MintToWithTopUp { - inner: MintTo, - max_top_up: u16, -} - -impl MintToWithTopUp { - pub fn instruction(self) -> Result { - self.inner.build_instruction(Some(self.max_top_up)) - } -} diff --git a/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs b/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs index 8ce6c148cb..e122801f7f 100644 --- a/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs +++ b/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs @@ -113,18 +113,9 @@ impl<'info> From<&MintToCheckedCpi<'info>> for MintToChecked { } } -impl MintToChecked { - pub fn with_max_top_up(self, max_top_up: u16) -> MintToCheckedWithTopUp { - MintToCheckedWithTopUp { - inner: self, - max_top_up, - } - } - - pub fn instruction(self) -> Result { - self.build_instruction(None) - } +impl_with_top_up!(MintToChecked, MintToCheckedWithTopUp); +impl MintToChecked { fn build_instruction(self, max_top_up: Option) -> Result { let accounts = vec![ AccountMeta::new(self.mint, false), @@ -148,14 +139,3 @@ impl MintToChecked { }) } } - -pub struct MintToCheckedWithTopUp { - inner: MintToChecked, - max_top_up: u16, -} - -impl MintToCheckedWithTopUp { - pub fn instruction(self) -> Result { - self.inner.build_instruction(Some(self.max_top_up)) - } -} diff --git a/sdk-libs/token-sdk/src/instruction/mod.rs b/sdk-libs/token-sdk/src/instruction/mod.rs index 7e1f7d2971..898ae2d998 100644 --- a/sdk-libs/token-sdk/src/instruction/mod.rs +++ b/sdk-libs/token-sdk/src/instruction/mod.rs @@ -91,6 +91,9 @@ //! ``` //! +#[macro_use] +mod macros; + mod approve; mod burn; mod burn_checked; diff --git a/sdk-libs/token-sdk/src/instruction/transfer.rs b/sdk-libs/token-sdk/src/instruction/transfer.rs index 944b824906..3c3ad5363f 100644 --- a/sdk-libs/token-sdk/src/instruction/transfer.rs +++ b/sdk-libs/token-sdk/src/instruction/transfer.rs @@ -103,18 +103,9 @@ impl<'info> From<&TransferCpi<'info>> for Transfer { } } -impl Transfer { - pub fn with_max_top_up(self, max_top_up: u16) -> TransferWithTopUp { - TransferWithTopUp { - inner: self, - max_top_up, - } - } - - pub fn instruction(self) -> Result { - self.build_instruction(None) - } +impl_with_top_up!(Transfer, TransferWithTopUp); +impl Transfer { fn build_instruction(self, max_top_up: Option) -> Result { let accounts = vec![ AccountMeta::new(self.source, false), @@ -137,14 +128,3 @@ impl Transfer { }) } } - -pub struct TransferWithTopUp { - inner: Transfer, - max_top_up: u16, -} - -impl TransferWithTopUp { - pub fn instruction(self) -> Result { - self.inner.build_instruction(Some(self.max_top_up)) - } -} diff --git a/sdk-libs/token-sdk/src/instruction/transfer_checked.rs b/sdk-libs/token-sdk/src/instruction/transfer_checked.rs index 09df622bde..d9d614c625 100644 --- a/sdk-libs/token-sdk/src/instruction/transfer_checked.rs +++ b/sdk-libs/token-sdk/src/instruction/transfer_checked.rs @@ -117,18 +117,9 @@ impl<'info> From<&TransferCheckedCpi<'info>> for TransferChecked { } } -impl TransferChecked { - pub fn with_max_top_up(self, max_top_up: u16) -> TransferCheckedWithTopUp { - TransferCheckedWithTopUp { - inner: self, - max_top_up, - } - } - - pub fn instruction(self) -> Result { - self.build_instruction(None) - } +impl_with_top_up!(TransferChecked, TransferCheckedWithTopUp); +impl TransferChecked { fn build_instruction(self, max_top_up: Option) -> Result { let accounts = vec![ AccountMeta::new(self.source, false), @@ -153,14 +144,3 @@ impl TransferChecked { }) } } - -pub struct TransferCheckedWithTopUp { - inner: TransferChecked, - max_top_up: u16, -} - -impl TransferCheckedWithTopUp { - pub fn instruction(self) -> Result { - self.inner.build_instruction(Some(self.max_top_up)) - } -} From 002e9c86815715f8a9f74c5fd174baf7eff040d5 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Wed, 25 Feb 2026 00:46:07 +0000 Subject: [PATCH 25/26] fix: make PDA authority readonly, regenerate compressed-token-sdk README --- sdk-libs/compressed-token-sdk/README.md | 20 +-- sdk-libs/compressed-token-sdk/src/lib.rs | 20 +-- .../src/instruction/approve.rs | 2 +- .../token-pinocchio/src/instruction/burn.rs | 2 +- .../src/instruction/burn_checked.rs | 2 +- .../token-pinocchio/src/instruction/close.rs | 2 +- .../src/instruction/create_ata.rs | 2 +- .../src/instruction/create_mint.rs | 2 +- .../src/instruction/create_mints.rs | 2 +- .../token-pinocchio/src/instruction/freeze.rs | 2 +- .../src/instruction/mint_to.rs | 2 +- .../src/instruction/mint_to_checked.rs | 2 +- .../token-pinocchio/src/instruction/revoke.rs | 2 +- .../src/instruction/transfer.rs | 2 +- .../src/instruction/transfer_checked.rs | 2 +- .../src/instruction/transfer_from_spl.rs | 8 +- .../src/instruction/transfer_to_spl.rs | 8 +- .../tests/test_transfer_checked.rs | 136 +++++++++++++++++- .../tests/test_transfer_checked.rs | 131 ++++++++++++++++- 19 files changed, 306 insertions(+), 43 deletions(-) diff --git a/sdk-libs/compressed-token-sdk/README.md b/sdk-libs/compressed-token-sdk/README.md index 057183ad22..72476fdea1 100644 --- a/sdk-libs/compressed-token-sdk/README.md +++ b/sdk-libs/compressed-token-sdk/README.md @@ -1,13 +1,13 @@ -# Light Compressed Token SDK +# Light Token SDK -Low-level SDK for compressed token operations on Light Protocol. +Low-level SDK for light token operations on Light Protocol. -This crate provides the core building blocks for working with compressed token accounts, +This crate provides the core building blocks for working with light token accounts, including instruction builders for transfers, mints, and compress/decompress operations. -## Compressed Token Accounts +## Light Token Accounts - are on Solana mainnet. - are compressed accounts. - can hold Light Mint and SPL Mint tokens. @@ -16,14 +16,14 @@ including instruction builders for transfers, mints, and compress/decompress ope ## Difference to Light-Token: light-token: Solana account that holds token balances of light-mints, SPL or Token 22 mints. -Compressed token: Compressed account storing token data. Rent-free, for storage and distribution. +Compressed light token: Compressed account storing token data. Rent-free, for storage and distribution. ## Features -- `v1` - Enable v1 compressed token support +- `v1` - Enable v1 light token support - `anchor` - Enable Anchor framework integration -For full examples, see the [Compressed Token Examples](https://github.com/Lightprotocol/examples-zk-compression). +For full examples, see the [Light Token Examples](https://github.com/Lightprotocol/examples-zk-compression). ## Operations reference @@ -38,7 +38,7 @@ For full examples, see the [Compressed Token Examples](https://github.com/Lightp | Compress SPL account | [create-compressed-token-accounts](https://www.zkcompression.com/compressed-tokens/guides/create-compressed-token-accounts) | [example](https://github.com/Lightprotocol/examples-zk-compression/blob/main/compressed-token-cookbook/actions/compress-spl-account.ts) | | Decompress | [create-compressed-token-accounts](https://www.zkcompression.com/compressed-tokens/guides/create-compressed-token-accounts) | [example](https://github.com/Lightprotocol/examples-zk-compression/blob/main/compressed-token-cookbook/actions/decompress.ts) | | Merge token accounts | [create-compressed-token-accounts](https://www.zkcompression.com/compressed-tokens/guides/create-compressed-token-accounts) | [example](https://github.com/Lightprotocol/examples-zk-compression/blob/main/compressed-token-cookbook/actions/merge-token-accounts.ts) | -| Create token pool | [create-compressed-token-accounts](https://www.zkcompression.com/compressed-tokens/guides/create-compressed-token-accounts) | [example](https://github.com/Lightprotocol/examples-zk-compression/blob/main/compressed-token-cookbook/actions/create-token-pool.ts) | +| Create SPL interface PDA | [create-compressed-token-accounts](https://www.zkcompression.com/compressed-tokens/guides/create-compressed-token-accounts) | [example](https://github.com/Lightprotocol/examples-zk-compression/blob/main/compressed-token-cookbook/actions/create-token-pool.ts) | ### Toolkit guides @@ -49,8 +49,8 @@ For full examples, see the [Compressed Token Examples](https://github.com/Lightp ## Modules -- [`compressed_token`] - Core compressed token types and instruction builders -- [`error`] - Error types for compressed token operations +- [`compressed_token`] - Core light token types and instruction builders +- [`error`] - Error types for light token operations - [`utils`] - Utility functions and default account configurations - [`constants`] - Program IDs and other constants - [`spl_interface`] - SPL interface PDA derivation utilities diff --git a/sdk-libs/compressed-token-sdk/src/lib.rs b/sdk-libs/compressed-token-sdk/src/lib.rs index 8074910c65..ea18db0e27 100644 --- a/sdk-libs/compressed-token-sdk/src/lib.rs +++ b/sdk-libs/compressed-token-sdk/src/lib.rs @@ -1,11 +1,11 @@ -//! # Light Compressed Token SDK +//! # Light Token SDK //! -//! Low-level SDK for compressed token operations on Light Protocol. +//! Low-level SDK for light token operations on Light Protocol. //! -//! This crate provides the core building blocks for working with compressed token accounts, +//! This crate provides the core building blocks for working with light token accounts, //! including instruction builders for transfers, mints, and compress/decompress operations. //! -//! ## Compressed Token Accounts +//! ## Light Token Accounts //| - do not require a rent-exempt balance. //! - are on Solana mainnet. //! - are compressed accounts. @@ -15,14 +15,14 @@ //! //! ## Difference to Light-Token: //! light-token: Solana account that holds token balances of light-mints, SPL or Token 22 mints. -//! Compressed token: Compressed account storing token data. Rent-free, for storage and distribution. +//! Compressed light token: Compressed account storing token data. Rent-free, for storage and distribution. //! //! ## Features //! -//! - `v1` - Enable v1 compressed token support +//! - `v1` - Enable v1 light token support //! - `anchor` - Enable Anchor framework integration //! -//! For full examples, see the [Compressed Token Examples](https://github.com/Lightprotocol/examples-zk-compression). +//! For full examples, see the [Light Token Examples](https://github.com/Lightprotocol/examples-zk-compression). //! //! ## Operations reference //! @@ -37,7 +37,7 @@ //! | Compress SPL account | [create-compressed-token-accounts](https://www.zkcompression.com/compressed-tokens/guides/create-compressed-token-accounts) | [example](https://github.com/Lightprotocol/examples-zk-compression/blob/main/compressed-token-cookbook/actions/compress-spl-account.ts) | //! | Decompress | [create-compressed-token-accounts](https://www.zkcompression.com/compressed-tokens/guides/create-compressed-token-accounts) | [example](https://github.com/Lightprotocol/examples-zk-compression/blob/main/compressed-token-cookbook/actions/decompress.ts) | //! | Merge token accounts | [create-compressed-token-accounts](https://www.zkcompression.com/compressed-tokens/guides/create-compressed-token-accounts) | [example](https://github.com/Lightprotocol/examples-zk-compression/blob/main/compressed-token-cookbook/actions/merge-token-accounts.ts) | -//! | Create token pool | [create-compressed-token-accounts](https://www.zkcompression.com/compressed-tokens/guides/create-compressed-token-accounts) | [example](https://github.com/Lightprotocol/examples-zk-compression/blob/main/compressed-token-cookbook/actions/create-token-pool.ts) | +//! | Create SPL interface PDA | [create-compressed-token-accounts](https://www.zkcompression.com/compressed-tokens/guides/create-compressed-token-accounts) | [example](https://github.com/Lightprotocol/examples-zk-compression/blob/main/compressed-token-cookbook/actions/create-token-pool.ts) | //! //! ### Toolkit guides //! @@ -48,8 +48,8 @@ //! //! ## Modules //! -//! - [`compressed_token`] - Core compressed token types and instruction builders -//! - [`error`] - Error types for compressed token operations +//! - [`compressed_token`] - Core light token types and instruction builders +//! - [`error`] - Error types for light token operations //! - [`utils`] - Utility functions and default account configurations //! - [`constants`] - Program IDs and other constants //! - [`spl_interface`] - SPL interface PDA derivation utilities diff --git a/sdk-libs/token-pinocchio/src/instruction/approve.rs b/sdk-libs/token-pinocchio/src/instruction/approve.rs index 9639c08c86..784d963fcf 100644 --- a/sdk-libs/token-pinocchio/src/instruction/approve.rs +++ b/sdk-libs/token-pinocchio/src/instruction/approve.rs @@ -10,7 +10,7 @@ use pinocchio::{ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; -/// Approve ctoken via CPI. +/// Approve light-token via CPI. /// /// # Example /// diff --git a/sdk-libs/token-pinocchio/src/instruction/burn.rs b/sdk-libs/token-pinocchio/src/instruction/burn.rs index 7c0ba5103b..7b701dc07c 100644 --- a/sdk-libs/token-pinocchio/src/instruction/burn.rs +++ b/sdk-libs/token-pinocchio/src/instruction/burn.rs @@ -10,7 +10,7 @@ use pinocchio::{ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; -/// Burn ctoken via CPI. +/// Burn light-token via CPI. /// /// # Example /// diff --git a/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs b/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs index 52dc8f8817..c9c49cf08c 100644 --- a/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs +++ b/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs @@ -10,7 +10,7 @@ use pinocchio::{ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; -/// Burn ctoken checked via CPI. +/// Burn light-token checked via CPI. /// /// # Example /// diff --git a/sdk-libs/token-pinocchio/src/instruction/close.rs b/sdk-libs/token-pinocchio/src/instruction/close.rs index 80733cdb66..13de708931 100644 --- a/sdk-libs/token-pinocchio/src/instruction/close.rs +++ b/sdk-libs/token-pinocchio/src/instruction/close.rs @@ -8,7 +8,7 @@ use pinocchio::{ pubkey::Pubkey, }; -/// Close ctoken account via CPI. +/// Close light-token account via CPI. /// /// # Example /// diff --git a/sdk-libs/token-pinocchio/src/instruction/create_ata.rs b/sdk-libs/token-pinocchio/src/instruction/create_ata.rs index 1b4ba5d5cc..bf4e5d1464 100644 --- a/sdk-libs/token-pinocchio/src/instruction/create_ata.rs +++ b/sdk-libs/token-pinocchio/src/instruction/create_ata.rs @@ -1,4 +1,4 @@ -//! Create CToken ATA CPI builder for pinocchio. +//! Create light-token associated token account CPI builder for pinocchio. //! //! Re-exports the generic `CreateTokenAtaCpi` from `light_sdk_types` //! specialized for pinocchio's `AccountInfo`. diff --git a/sdk-libs/token-pinocchio/src/instruction/create_mint.rs b/sdk-libs/token-pinocchio/src/instruction/create_mint.rs index 6ec8281448..78ad5838c1 100644 --- a/sdk-libs/token-pinocchio/src/instruction/create_mint.rs +++ b/sdk-libs/token-pinocchio/src/instruction/create_mint.rs @@ -317,7 +317,7 @@ pub fn derive_compressed_address(mint: &[u8; 32]) -> [u8; 32] { ) } -/// Finds the compressed mint PDA address from a mint seed. +/// Finds the light mint PDA address from a mint seed. pub fn find_mint_address(mint_seed: &[u8; 32]) -> ([u8; 32], u8) { AccountInfo::find_program_address( &[COMPRESSED_MINT_SEED, mint_seed.as_ref()], diff --git a/sdk-libs/token-pinocchio/src/instruction/create_mints.rs b/sdk-libs/token-pinocchio/src/instruction/create_mints.rs index c246d49f3c..0a2f71ea62 100644 --- a/sdk-libs/token-pinocchio/src/instruction/create_mints.rs +++ b/sdk-libs/token-pinocchio/src/instruction/create_mints.rs @@ -71,7 +71,7 @@ pub use light_sdk_types::interface::cpi::create_mints::{ }; use pinocchio::account_info::AccountInfo; -/// High-level struct for creating compressed mints (pinocchio). +/// High-level struct for creating light mints (pinocchio). /// /// Type alias with pinocchio's `AccountInfo` already set. /// Consolidates proof parsing, tree account resolution, and CPI invocation. diff --git a/sdk-libs/token-pinocchio/src/instruction/freeze.rs b/sdk-libs/token-pinocchio/src/instruction/freeze.rs index 22b845d21e..ea44044b3a 100644 --- a/sdk-libs/token-pinocchio/src/instruction/freeze.rs +++ b/sdk-libs/token-pinocchio/src/instruction/freeze.rs @@ -10,7 +10,7 @@ use pinocchio::{ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; -/// Freeze ctoken via CPI. +/// Freeze light-token account via CPI. /// /// # Example /// diff --git a/sdk-libs/token-pinocchio/src/instruction/mint_to.rs b/sdk-libs/token-pinocchio/src/instruction/mint_to.rs index 780aa496e5..0ff6ec8deb 100644 --- a/sdk-libs/token-pinocchio/src/instruction/mint_to.rs +++ b/sdk-libs/token-pinocchio/src/instruction/mint_to.rs @@ -10,7 +10,7 @@ use pinocchio::{ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; -/// Mint to ctoken via CPI. +/// Mint to light-token account via CPI. /// /// # Example /// diff --git a/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs b/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs index a7d07f0c2f..cf84e2eefb 100644 --- a/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs +++ b/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs @@ -10,7 +10,7 @@ use pinocchio::{ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; -/// Mint to ctoken checked via CPI. +/// Mint to light-token account checked via CPI. /// /// # Example /// diff --git a/sdk-libs/token-pinocchio/src/instruction/revoke.rs b/sdk-libs/token-pinocchio/src/instruction/revoke.rs index 962a0f178a..84b9b20ea8 100644 --- a/sdk-libs/token-pinocchio/src/instruction/revoke.rs +++ b/sdk-libs/token-pinocchio/src/instruction/revoke.rs @@ -10,7 +10,7 @@ use pinocchio::{ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; -/// Revoke ctoken via CPI. +/// Revoke light-token via CPI. /// /// # Example /// diff --git a/sdk-libs/token-pinocchio/src/instruction/transfer.rs b/sdk-libs/token-pinocchio/src/instruction/transfer.rs index 223d16710b..797811261d 100644 --- a/sdk-libs/token-pinocchio/src/instruction/transfer.rs +++ b/sdk-libs/token-pinocchio/src/instruction/transfer.rs @@ -10,7 +10,7 @@ use pinocchio::{ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; -/// Transfer ctoken via CPI. +/// Transfer light-token via CPI. /// /// # Example /// diff --git a/sdk-libs/token-pinocchio/src/instruction/transfer_checked.rs b/sdk-libs/token-pinocchio/src/instruction/transfer_checked.rs index be7baac4e2..d142dfe35a 100644 --- a/sdk-libs/token-pinocchio/src/instruction/transfer_checked.rs +++ b/sdk-libs/token-pinocchio/src/instruction/transfer_checked.rs @@ -10,7 +10,7 @@ use pinocchio::{ use crate::constants::LIGHT_TOKEN_PROGRAM_ID; -/// Transfer ctoken checked via CPI. +/// Transfer light-token checked via CPI. /// /// # Example /// diff --git a/sdk-libs/token-pinocchio/src/instruction/transfer_from_spl.rs b/sdk-libs/token-pinocchio/src/instruction/transfer_from_spl.rs index 3532fac001..2b0cab17d7 100644 --- a/sdk-libs/token-pinocchio/src/instruction/transfer_from_spl.rs +++ b/sdk-libs/token-pinocchio/src/instruction/transfer_from_spl.rs @@ -43,7 +43,7 @@ pub struct TransferFromSplCpi<'info> { pub spl_interface_pda_bump: u8, pub decimals: u8, pub source_spl_token_account: &'info AccountInfo, - /// Destination ctoken account (writable) + /// Destination light-token account (writable) pub destination: &'info AccountInfo, pub authority: &'info AccountInfo, pub mint: &'info AccountInfo, @@ -82,8 +82,8 @@ impl<'info> TransferFromSplCpi<'info> { &self, ) -> Result<(Vec, Vec>, Vec<&AccountInfo>), ProgramError> { // Build compressions: - // 1. Wrap SPL tokens to Light Token pool - // 2. Unwrap from pool to destination ctoken account + // 1. Wrap SPL tokens via SPL interface PDA + // 2. Unwrap from pool to destination light-token account let wrap_from_spl = Compression::compress_spl( self.amount, 0, // mint index @@ -133,7 +133,7 @@ impl<'info> TransferFromSplCpi<'info> { // [1] fee_payer (signer, writable) // [2..] packed_accounts: // - [0] mint (readonly) - // - [1] destination ctoken account (writable) + // - [1] destination light-token account (writable) // - [2] authority (signer, readonly) // - [3] source SPL token account (writable) // - [4] SPL interface PDA (writable) diff --git a/sdk-libs/token-pinocchio/src/instruction/transfer_to_spl.rs b/sdk-libs/token-pinocchio/src/instruction/transfer_to_spl.rs index e3f2015cea..c60a2708ac 100644 --- a/sdk-libs/token-pinocchio/src/instruction/transfer_to_spl.rs +++ b/sdk-libs/token-pinocchio/src/instruction/transfer_to_spl.rs @@ -23,7 +23,7 @@ const TRANSFER2_DISCRIMINATOR: u8 = 101; /// # Example /// ```rust,ignore /// TransferToSplCpi { -/// source: &source_ctoken, +/// source: &source_light_token, /// destination_spl_token_account: &destination_spl, /// amount: 100, /// authority: &authority, @@ -78,12 +78,12 @@ impl<'info> TransferToSplCpi<'info> { &self, ) -> Result<(Vec, Vec>, Vec<&AccountInfo>), ProgramError> { // Build compressions: - // 1. Compress from ctoken account to pool + // 1. Transfer from light-token account via SPL interface PDA // 2. Decompress from pool to SPL token account let compress_to_pool = Compression::compress( self.amount, 0, // mint index - 1, // source ctoken account index + 1, // source light-token account index 3, // authority index ); @@ -129,7 +129,7 @@ impl<'info> TransferToSplCpi<'info> { // [1] fee_payer (signer, writable) // [2..] packed_accounts: // - [0] mint (readonly) - // - [1] source ctoken account (writable) + // - [1] source light-token account (writable) // - [2] destination SPL token account (writable) // - [3] authority (signer, readonly) // - [4] SPL interface PDA (writable) diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_checked.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_checked.rs index 38a2de75e1..39ff9f2f47 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_checked.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_checked.rs @@ -14,7 +14,7 @@ use light_token::{ spl_interface::{find_spl_interface_pda_with_index, CreateSplInterfacePda}, }; use light_token_interface::state::Token; -use sdk_light_token_pinocchio_test::{InstructionType, TransferCheckedData}; +use sdk_light_token_pinocchio_test::{InstructionType, TransferCheckedData, TOKEN_ACCOUNT_SEED}; use shared::*; use solana_sdk::{ instruction::{AccountMeta, Instruction}, @@ -350,3 +350,137 @@ async fn test_ctoken_transfer_checked_mint() { let dest_state = Token::deserialize(&mut &dest_data.data[..]).unwrap(); assert_eq!(dest_state.amount, 500); } + +/// Test transfer_checked using invoke_signed() with PDA authority +#[tokio::test] +async fn test_ctoken_transfer_checked_invoke_signed() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let decimals = 9u8; + + // Derive the PDA that will own the source account + let (pda_owner, _bump) = Pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &PROGRAM_ID); + let dest_owner = payer.pubkey(); + + let (mint, _compression_address, ata_pubkeys) = setup_create_mint_with_freeze_authority( + &mut rpc, + &payer, + payer.pubkey(), + None, + decimals, + vec![(1000, pda_owner), (0, dest_owner)], + ) + .await; + + let source_ata = ata_pubkeys[0]; + let dest_ata = ata_pubkeys[1]; + + // Transfer 300 tokens using invoke_signed + let transfer_data = TransferCheckedData { + amount: 300, + decimals, + }; + let mut instruction_data = vec![InstructionType::CTokenTransferCheckedInvokeSigned as u8]; + transfer_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = light_token::instruction::LIGHT_TOKEN_PROGRAM_ID; + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(source_ata, false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(dest_ata, false), + AccountMeta::new_readonly(pda_owner, false), // PDA authority (program signs) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(payer.pubkey(), true), // fee_payer + AccountMeta::new_readonly(light_token_program, false), + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify balances + let source_data = rpc.get_account(source_ata).await.unwrap().unwrap(); + let source_state = Token::deserialize(&mut &source_data.data[..]).unwrap(); + assert_eq!(source_state.amount, 700); + + let dest_data = rpc.get_account(dest_ata).await.unwrap().unwrap(); + let dest_state = Token::deserialize(&mut &dest_data.data[..]).unwrap(); + assert_eq!(dest_state.amount, 300); +} + +/// Test transfer_checked with separate fee_payer using invoke() +#[tokio::test] +async fn test_ctoken_transfer_checked_invoke_with_separate_fee_payer() { + use solana_sdk::signature::Keypair; + + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let decimals = 9u8; + + // Separate keypair as the source account owner (not the fee payer) + let owner_keypair = Keypair::new(); + let dest_owner = payer.pubkey(); + + let (mint, _compression_address, ata_pubkeys) = setup_create_mint_with_freeze_authority( + &mut rpc, + &payer, + payer.pubkey(), + None, + decimals, + vec![(1000, owner_keypair.pubkey()), (0, dest_owner)], + ) + .await; + + let source_ata = ata_pubkeys[0]; + let dest_ata = ata_pubkeys[1]; + + // Transfer 400 tokens with separate fee_payer + let transfer_data = TransferCheckedData { + amount: 400, + decimals, + }; + let mut instruction_data = vec![InstructionType::CTokenTransferCheckedInvoke as u8]; + transfer_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = light_token::instruction::LIGHT_TOKEN_PROGRAM_ID; + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(source_ata, false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(dest_ata, false), + AccountMeta::new_readonly(owner_keypair.pubkey(), true), // authority (readonly signer) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(payer.pubkey(), true), // fee_payer (separate) + AccountMeta::new_readonly(light_token_program, false), + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &owner_keypair]) + .await + .unwrap(); + + // Verify balances + let source_data = rpc.get_account(source_ata).await.unwrap().unwrap(); + let source_state = Token::deserialize(&mut &source_data.data[..]).unwrap(); + assert_eq!(source_state.amount, 600); + + let dest_data = rpc.get_account(dest_ata).await.unwrap().unwrap(); + let dest_state = Token::deserialize(&mut &dest_data.data[..]).unwrap(); + assert_eq!(dest_state.amount, 400); +} diff --git a/sdk-tests/sdk-light-token-test/tests/test_transfer_checked.rs b/sdk-tests/sdk-light-token-test/tests/test_transfer_checked.rs index 9d1a8ca283..3e4042de90 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_transfer_checked.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_transfer_checked.rs @@ -14,7 +14,7 @@ use light_token::{ spl_interface::{find_spl_interface_pda_with_index, CreateSplInterfacePda}, }; use light_token_interface::state::Token; -use sdk_light_token_test::{InstructionType, TransferCheckedData, ID}; +use sdk_light_token_test::{InstructionType, TransferCheckedData, ID, TOKEN_ACCOUNT_SEED}; use shared::*; use solana_sdk::{ instruction::{AccountMeta, Instruction}, @@ -341,3 +341,132 @@ async fn test_ctoken_transfer_checked_mint() { let dest_state = Token::deserialize(&mut &dest_data.data[..]).unwrap(); assert_eq!(dest_state.amount, 500); } + +/// Test transfer_checked using invoke_signed() with PDA authority +#[tokio::test] +async fn test_ctoken_transfer_checked_invoke_signed() { + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let decimals = 9u8; + + // Derive the PDA that will own the source account + let (pda_owner, _bump) = Pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &ID); + let dest_owner = payer.pubkey(); + + let (mint, _compression_address, ata_pubkeys) = setup_create_mint_with_freeze_authority( + &mut rpc, + &payer, + payer.pubkey(), + None, + decimals, + vec![(1000, pda_owner), (0, dest_owner)], + ) + .await; + + let source_ata = ata_pubkeys[0]; + let dest_ata = ata_pubkeys[1]; + + // Transfer 300 tokens using invoke_signed + let transfer_data = TransferCheckedData { + amount: 300, + decimals, + }; + let mut instruction_data = vec![InstructionType::CTokenTransferCheckedInvokeSigned as u8]; + transfer_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = light_token::instruction::LIGHT_TOKEN_PROGRAM_ID; + let instruction = Instruction { + program_id: ID, + accounts: vec![ + AccountMeta::new(source_ata, false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(dest_ata, false), + AccountMeta::new_readonly(pda_owner, false), // PDA authority, not signer + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(payer.pubkey(), true), // fee_payer + AccountMeta::new_readonly(light_token_program, false), + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify balances + let source_data = rpc.get_account(source_ata).await.unwrap().unwrap(); + let source_state = Token::deserialize(&mut &source_data.data[..]).unwrap(); + assert_eq!(source_state.amount, 700); + + let dest_data = rpc.get_account(dest_ata).await.unwrap().unwrap(); + let dest_state = Token::deserialize(&mut &dest_data.data[..]).unwrap(); + assert_eq!(dest_state.amount, 300); +} + +/// Test transfer_checked with separate fee_payer using invoke() +#[tokio::test] +async fn test_ctoken_transfer_checked_invoke_with_separate_fee_payer() { + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let decimals = 9u8; + + let owner_keypair = Keypair::new(); + rpc.airdrop_lamports(&owner_keypair.pubkey(), 1_000_000_000) + .await + .unwrap(); + + let dest_owner = Pubkey::new_unique(); + + let (mint, _compression_address, ata_pubkeys) = setup_create_mint_with_freeze_authority( + &mut rpc, + &payer, + payer.pubkey(), + None, + decimals, + vec![(1000, owner_keypair.pubkey()), (0, dest_owner)], + ) + .await; + + let source_ata = ata_pubkeys[0]; + let dest_ata = ata_pubkeys[1]; + + // Transfer 400 tokens with separate fee_payer + let transfer_data = TransferCheckedData { + amount: 400, + decimals, + }; + let mut instruction_data = vec![InstructionType::CTokenTransferCheckedInvoke as u8]; + transfer_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = light_token::instruction::LIGHT_TOKEN_PROGRAM_ID; + let instruction = Instruction { + program_id: ID, + accounts: vec![ + AccountMeta::new(source_ata, false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(dest_ata, false), + AccountMeta::new_readonly(owner_keypair.pubkey(), true), // authority (signer, not fee_payer) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(payer.pubkey(), true), // fee_payer (separate) + AccountMeta::new_readonly(light_token_program, false), + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &owner_keypair]) + .await + .unwrap(); + + // Verify balances + let source_data = rpc.get_account(source_ata).await.unwrap().unwrap(); + let source_state = Token::deserialize(&mut &source_data.data[..]).unwrap(); + assert_eq!(source_state.amount, 600); + + let dest_data = rpc.get_account(dest_ata).await.unwrap().unwrap(); + let dest_state = Token::deserialize(&mut &dest_data.data[..]).unwrap(); + assert_eq!(dest_state.amount, 400); +} From 2e1075925e695bde186eca446410a76c431937d3 Mon Sep 17 00:00:00 2001 From: ananas Date: Thu, 26 Feb 2026 00:43:05 +0000 Subject: [PATCH 26/26] fix: pinocchio sdk inconsistencies --- .../src/instruction/compressible.rs | 128 +++++++++ .../token-pinocchio/src/instruction/create.rs | 189 ++++++++++++- .../src/instruction/create_ata.rs | 263 +++++++++++++++++- .../token-pinocchio/src/instruction/mod.rs | 2 + .../src/instruction/transfer_interface.rs | 84 ++++-- sdk-libs/token-pinocchio/src/lib.rs | 1 - .../src/create_ata.rs | 87 +++++- .../src/create_token_account.rs | 96 ++++++- .../sdk-light-token-pinocchio/src/lib.rs | 38 ++- .../tests/test_create_ata.rs | 182 +++++++++--- .../tests/test_create_token_account.rs | 187 ++++++++++--- 11 files changed, 1141 insertions(+), 116 deletions(-) create mode 100644 sdk-libs/token-pinocchio/src/instruction/compressible.rs diff --git a/sdk-libs/token-pinocchio/src/instruction/compressible.rs b/sdk-libs/token-pinocchio/src/instruction/compressible.rs new file mode 100644 index 0000000000..7f776000dc --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/compressible.rs @@ -0,0 +1,128 @@ +//! Parameters for creating compressible ctoken accounts. +//! +//! Compressible accounts have sponsored rent and can be compressed to compressed +//! token accounts when their lamports balance is insufficient. + +use light_token_interface::{instructions::extensions::CompressToPubkey, state::TokenDataVersion}; +use pinocchio::account_info::AccountInfo; + +use crate::constants::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; + +/// Parameters for creating compressible ctoken accounts. +/// +/// Compressible accounts have sponsored rent and can be compressed to compressed +/// token accounts when their lamports balance is insufficient. +/// +/// Default values are: +/// - 24 hours rent +/// - lamports for 3 hours rent (paid on transfer when account rent is insufficient to cover the next 2 epochs) +/// - Protocol rent sponsor +/// - TokenDataVersion::ShaFlat token data hashing (only sha is supported for compressible accounts) +#[derive(Debug, Clone)] +pub struct CompressibleParams { + pub token_account_version: TokenDataVersion, + pub pre_pay_num_epochs: u8, + /// Number of lamports transferred on a write operation (eg transfer) when account rent is + /// insufficient to cover the next 2 rent-epochs. + /// Default: 766 lamports for 3 hours rent. + pub lamports_per_write: Option, + pub compress_to_account_pubkey: Option, + pub compressible_config: [u8; 32], + pub rent_sponsor: [u8; 32], + pub compression_only: bool, +} + +impl Default for CompressibleParams { + fn default() -> Self { + Self { + compressible_config: LIGHT_TOKEN_CONFIG, + rent_sponsor: LIGHT_TOKEN_RENT_SPONSOR, + pre_pay_num_epochs: 16, + lamports_per_write: Some(766), + compress_to_account_pubkey: None, + token_account_version: TokenDataVersion::ShaFlat, + compression_only: false, + } + } +} + +impl CompressibleParams { + /// Creates a new `CompressibleParams` with default values. + /// - 24 hours rent + /// - 3 hours top up (paid on transfer when account rent is insufficient to cover the next 2 epochs) + /// - Protocol rent sponsor + /// - TokenDataVersion::ShaFlat token data hashing + pub fn new() -> Self { + Self::default() + } + + /// Creates default params for ATAs (compression_only = true). + /// ATAs are always compression_only. + pub fn default_ata() -> Self { + Self { + compression_only: true, + ..Self::default() + } + } + + /// Sets the destination pubkey for compression. + pub fn compress_to_pubkey(mut self, compress_to: CompressToPubkey) -> Self { + self.compress_to_account_pubkey = Some(compress_to); + self + } +} + +/// Parameters for creating compressible ctoken accounts via CPI. +pub struct CompressibleParamsCpi<'info> { + pub compressible_config: &'info AccountInfo, + pub rent_sponsor: &'info AccountInfo, + pub system_program: &'info AccountInfo, + pub pre_pay_num_epochs: u8, + pub lamports_per_write: Option, + pub compress_to_account_pubkey: Option, + pub token_account_version: TokenDataVersion, + pub compression_only: bool, +} + +impl<'info> CompressibleParamsCpi<'info> { + pub fn new( + compressible_config: &'info AccountInfo, + rent_sponsor: &'info AccountInfo, + system_program: &'info AccountInfo, + ) -> Self { + let defaults = CompressibleParams::default(); + Self { + compressible_config, + rent_sponsor, + system_program, + pre_pay_num_epochs: defaults.pre_pay_num_epochs, + lamports_per_write: defaults.lamports_per_write, + compress_to_account_pubkey: None, + token_account_version: defaults.token_account_version, + compression_only: defaults.compression_only, + } + } + + pub fn new_ata( + compressible_config: &'info AccountInfo, + rent_sponsor: &'info AccountInfo, + system_program: &'info AccountInfo, + ) -> Self { + let defaults = CompressibleParams::default_ata(); + Self { + compressible_config, + rent_sponsor, + system_program, + pre_pay_num_epochs: defaults.pre_pay_num_epochs, + lamports_per_write: defaults.lamports_per_write, + compress_to_account_pubkey: None, + token_account_version: defaults.token_account_version, + compression_only: defaults.compression_only, + } + } + + pub fn with_compress_to_pubkey(mut self, compress_to: CompressToPubkey) -> Self { + self.compress_to_account_pubkey = Some(compress_to); + self + } +} diff --git a/sdk-libs/token-pinocchio/src/instruction/create.rs b/sdk-libs/token-pinocchio/src/instruction/create.rs index 94a5430e4b..f1695a3ac5 100644 --- a/sdk-libs/token-pinocchio/src/instruction/create.rs +++ b/sdk-libs/token-pinocchio/src/instruction/create.rs @@ -1,9 +1,186 @@ //! Create CToken account CPI builder for pinocchio. -//! -//! Re-exports the generic `CreateTokenAccountCpi` from `light_sdk_types` -//! specialized for pinocchio's `AccountInfo`. -// TODO: add types with generics set so that we dont expose the generics -pub use light_sdk_types::interface::cpi::create_token_accounts::{ - CreateTokenAccountCpi, CreateTokenAccountRentFreeCpi, +use alloc::vec::Vec; + +use borsh::BorshSerialize; +use light_token_interface::{ + instructions::{ + create_token_account::CreateTokenAccountInstructionData, + extensions::{CompressToPubkey, CompressibleExtensionInstructionData}, + }, + LIGHT_TOKEN_PROGRAM_ID, +}; +use pinocchio::{ + account_info::AccountInfo, + cpi::slice_invoke_signed, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, }; + +use super::compressible::CompressibleParamsCpi; + +/// Discriminator for `InitializeAccount3` (create token account). +const CREATE_TOKEN_ACCOUNT_DISCRIMINATOR: u8 = 18; + +/// CPI builder for creating CToken accounts (vaults). +/// +/// # Example - Rent-free vault with PDA signing +/// ```rust,ignore +/// CreateTokenAccountCpi { +/// payer: &ctx.accounts.payer, +/// account: &ctx.accounts.vault, +/// mint: &ctx.accounts.mint, +/// owner: ctx.accounts.vault_authority.key().clone(), +/// } +/// .rent_free( +/// &ctx.accounts.ctoken_config, +/// &ctx.accounts.rent_sponsor, +/// &ctx.accounts.system_program, +/// ) +/// .invoke_signed(&[Signer::from(&[b"vault", mint_key.as_ref(), &[bump]])])?; +/// ``` +pub struct CreateTokenAccountCpi<'info> { + pub payer: &'info AccountInfo, + pub account: &'info AccountInfo, + pub mint: &'info AccountInfo, + pub owner: [u8; 32], +} + +impl<'info> CreateTokenAccountCpi<'info> { + /// Enable rent-free mode with compressible config. + /// + /// Returns a builder that can call `.invoke()` or `.invoke_signed(signers)`. + /// When using `invoke_signed`, the seeds are used for both PDA signing + /// and deriving the compress_to address. + pub fn rent_free( + self, + config: &'info AccountInfo, + sponsor: &'info AccountInfo, + system_program: &'info AccountInfo, + ) -> CreateTokenAccountRentFreeCpi<'info> { + CreateTokenAccountRentFreeCpi { + base: self, + config, + sponsor, + system_program, + } + } + + /// Invoke without rent-free (requires manually constructed compressible params). + pub fn invoke_with( + self, + compressible: CompressibleParamsCpi<'info>, + ) -> Result<(), ProgramError> { + let (data, metas, account_infos) = build_instruction_inner(&self, &compressible, None)?; + invoke_cpi(&data, &metas, &account_infos, &[]) + } + + /// Invoke with signing, without rent-free (requires manually constructed compressible params). + pub fn invoke_signed_with( + self, + compressible: CompressibleParamsCpi<'info>, + signers: &[Signer], + ) -> Result<(), ProgramError> { + let (data, metas, account_infos) = build_instruction_inner(&self, &compressible, None)?; + invoke_cpi(&data, &metas, &account_infos, signers) + } +} + +/// Rent-free enabled CToken account creation CPI. +pub struct CreateTokenAccountRentFreeCpi<'info> { + base: CreateTokenAccountCpi<'info>, + config: &'info AccountInfo, + sponsor: &'info AccountInfo, + system_program: &'info AccountInfo, +} + +impl<'info> CreateTokenAccountRentFreeCpi<'info> { + /// Invoke CPI for non-program-owned accounts. + pub fn invoke(self) -> Result<(), ProgramError> { + let compressible = + CompressibleParamsCpi::new(self.config, self.sponsor, self.system_program); + let (data, metas, account_infos) = + build_instruction_inner(&self.base, &compressible, None)?; + invoke_cpi(&data, &metas, &account_infos, &[]) + } + + /// Invoke CPI with PDA signing for program-owned accounts. + /// + /// For compress_to derivation, use `CreateTokenAccountCpi::invoke_signed_with()` + /// with a `CompressibleParamsCpi` that has `compress_to_account_pubkey` set. + pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { + let compressible = + CompressibleParamsCpi::new(self.config, self.sponsor, self.system_program); + let (data, metas, account_infos) = + build_instruction_inner(&self.base, &compressible, None)?; + invoke_cpi(&data, &metas, &account_infos, signers) + } +} + +/// Build instruction data, account metas, and account infos for CreateTokenAccount. +#[allow(clippy::type_complexity)] +fn build_instruction_inner<'a>( + base: &CreateTokenAccountCpi<'a>, + compressible: &CompressibleParamsCpi<'a>, + compress_to: Option, +) -> Result<(Vec, [AccountMeta<'a>; 6], [&'a AccountInfo; 6]), ProgramError> { + let instruction_data = CreateTokenAccountInstructionData { + owner: base.owner.into(), + compressible_config: Some(CompressibleExtensionInstructionData { + token_account_version: compressible.token_account_version as u8, + rent_payment: compressible.pre_pay_num_epochs, + compression_only: compressible.compression_only as u8, + write_top_up: compressible.lamports_per_write.unwrap_or(0), + compress_to_account_pubkey: compress_to + .or_else(|| compressible.compress_to_account_pubkey.clone()), + }), + }; + + let mut data = Vec::new(); + data.push(CREATE_TOKEN_ACCOUNT_DISCRIMINATOR); + instruction_data + .serialize(&mut data) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + // Account order matches the cToken program: + // [0] account (signer, writable) + // [1] mint (readonly) + // [2] payer (signer, writable) + // [3] compressible_config (readonly) + // [4] system_program (readonly) + // [5] rent_sponsor (writable) + let metas = [ + AccountMeta::writable_signer(base.account.key()), + AccountMeta::readonly(base.mint.key()), + AccountMeta::writable_signer(base.payer.key()), + AccountMeta::readonly(compressible.compressible_config.key()), + AccountMeta::readonly(compressible.system_program.key()), + AccountMeta::writable(compressible.rent_sponsor.key()), + ]; + + let account_infos = [ + base.account, + base.mint, + base.payer, + compressible.compressible_config, + compressible.system_program, + compressible.rent_sponsor, + ]; + + Ok((data, metas, account_infos)) +} + +/// Helper to invoke CPI to Light Token program. +fn invoke_cpi( + data: &[u8], + metas: &[AccountMeta], + account_infos: &[&AccountInfo], + signers: &[Signer], +) -> Result<(), ProgramError> { + let instruction = Instruction { + program_id: &LIGHT_TOKEN_PROGRAM_ID, + accounts: metas, + data, + }; + slice_invoke_signed(&instruction, account_infos, signers) +} diff --git a/sdk-libs/token-pinocchio/src/instruction/create_ata.rs b/sdk-libs/token-pinocchio/src/instruction/create_ata.rs index bf4e5d1464..14f7320168 100644 --- a/sdk-libs/token-pinocchio/src/instruction/create_ata.rs +++ b/sdk-libs/token-pinocchio/src/instruction/create_ata.rs @@ -1,21 +1,33 @@ //! Create light-token associated token account CPI builder for pinocchio. -//! -//! Re-exports the generic `CreateTokenAtaCpi` from `light_sdk_types` -//! specialized for pinocchio's `AccountInfo`. +use alloc::vec::Vec; + +use borsh::BorshSerialize; use light_account_checks::AccountInfoTrait; -// TODO: add types with generics set so that we dont expose the generics -pub use light_sdk_types::interface::cpi::create_token_accounts::{ - CreateTokenAtaCpi, CreateTokenAtaCpiIdempotent, CreateTokenAtaRentFreeCpi, +use light_token_interface::{ + instructions::{ + create_associated_token_account::CreateAssociatedTokenAccountInstructionData, + extensions::CompressibleExtensionInstructionData, + }, + LIGHT_TOKEN_PROGRAM_ID, +}; +use pinocchio::{ + account_info::AccountInfo, + cpi::slice_invoke_signed, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, }; -use light_token_interface::LIGHT_TOKEN_PROGRAM_ID; -use pinocchio::account_info::AccountInfo; + +use super::compressible::CompressibleParamsCpi; + +/// Discriminator for `CreateAssociatedTokenAccount`. +const CREATE_ATA_DISCRIMINATOR: u8 = 100; +/// Discriminator for `CreateAssociatedTokenAccountIdempotent`. +const CREATE_ATA_IDEMPOTENT_DISCRIMINATOR: u8 = 102; /// Derive the associated token account address for a given owner and mint. /// /// Returns `[u8; 32]` -- the ATA address. -/// -/// Uses pinocchio's `AccountInfo` for PDA derivation. pub fn derive_associated_token_account(owner: &[u8; 32], mint: &[u8; 32]) -> [u8; 32] { AccountInfo::find_program_address( &[ @@ -27,3 +39,234 @@ pub fn derive_associated_token_account(owner: &[u8; 32], mint: &[u8; 32]) -> [u8 ) .0 } + +/// CPI builder for creating CToken ATAs. +/// +/// # Example - Rent-free ATA (idempotent) +/// ```rust,ignore +/// CreateTokenAtaCpi { +/// payer: &ctx.accounts.payer, +/// owner: &ctx.accounts.owner, +/// mint: &ctx.accounts.mint, +/// ata: &ctx.accounts.user_ata, +/// } +/// .idempotent() +/// .rent_free( +/// &ctx.accounts.ctoken_config, +/// &ctx.accounts.rent_sponsor, +/// &ctx.accounts.system_program, +/// ) +/// .invoke()?; +/// ``` +pub struct CreateTokenAtaCpi<'info> { + pub payer: &'info AccountInfo, + pub owner: &'info AccountInfo, + pub mint: &'info AccountInfo, + pub ata: &'info AccountInfo, +} + +impl<'info> CreateTokenAtaCpi<'info> { + /// Make this an idempotent create (won't fail if ATA already exists). + pub fn idempotent(self) -> CreateTokenAtaCpiIdempotent<'info> { + CreateTokenAtaCpiIdempotent { base: self } + } + + /// Enable rent-free mode with compressible config. + pub fn rent_free( + self, + config: &'info AccountInfo, + sponsor: &'info AccountInfo, + system_program: &'info AccountInfo, + ) -> CreateTokenAtaRentFreeCpi<'info> { + CreateTokenAtaRentFreeCpi { + payer: self.payer, + owner: self.owner, + mint: self.mint, + ata: self.ata, + idempotent: false, + config, + sponsor, + system_program, + } + } + + /// Invoke without rent-free (requires manually constructed compressible params). + pub fn invoke_with( + self, + compressible: CompressibleParamsCpi<'info>, + ) -> Result<(), ProgramError> { + let (data, metas, account_infos) = build_create_ata_instruction_inner( + self.owner, + self.mint, + self.payer, + self.ata, + &compressible, + false, + )?; + invoke_cpi(&data, &metas, &account_infos, &[]) + } +} + +/// Idempotent ATA creation (intermediate type). +pub struct CreateTokenAtaCpiIdempotent<'info> { + base: CreateTokenAtaCpi<'info>, +} + +impl<'info> CreateTokenAtaCpiIdempotent<'info> { + /// Enable rent-free mode with compressible config. + pub fn rent_free( + self, + config: &'info AccountInfo, + sponsor: &'info AccountInfo, + system_program: &'info AccountInfo, + ) -> CreateTokenAtaRentFreeCpi<'info> { + CreateTokenAtaRentFreeCpi { + payer: self.base.payer, + owner: self.base.owner, + mint: self.base.mint, + ata: self.base.ata, + idempotent: true, + config, + sponsor, + system_program, + } + } + + /// Invoke without rent-free (requires manually constructed compressible params). + pub fn invoke_with( + self, + compressible: CompressibleParamsCpi<'info>, + ) -> Result<(), ProgramError> { + let (data, metas, account_infos) = build_create_ata_instruction_inner( + self.base.owner, + self.base.mint, + self.base.payer, + self.base.ata, + &compressible, + true, + )?; + invoke_cpi(&data, &metas, &account_infos, &[]) + } +} + +/// Rent-free enabled CToken ATA creation CPI. +pub struct CreateTokenAtaRentFreeCpi<'info> { + payer: &'info AccountInfo, + owner: &'info AccountInfo, + mint: &'info AccountInfo, + ata: &'info AccountInfo, + idempotent: bool, + config: &'info AccountInfo, + sponsor: &'info AccountInfo, + system_program: &'info AccountInfo, +} + +impl<'info> CreateTokenAtaRentFreeCpi<'info> { + /// Invoke CPI. + pub fn invoke(self) -> Result<(), ProgramError> { + let compressible = + CompressibleParamsCpi::new_ata(self.config, self.sponsor, self.system_program); + let (data, metas, account_infos) = build_create_ata_instruction_inner( + self.owner, + self.mint, + self.payer, + self.ata, + &compressible, + self.idempotent, + )?; + invoke_cpi(&data, &metas, &account_infos, &[]) + } + + /// Invoke CPI with signer seeds. + pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { + let compressible = + CompressibleParamsCpi::new_ata(self.config, self.sponsor, self.system_program); + let (data, metas, account_infos) = build_create_ata_instruction_inner( + self.owner, + self.mint, + self.payer, + self.ata, + &compressible, + self.idempotent, + )?; + invoke_cpi(&data, &metas, &account_infos, signers) + } +} + +/// Build instruction data, account metas, and account infos for CreateAssociatedTokenAccount. +#[allow(clippy::type_complexity)] +fn build_create_ata_instruction_inner<'a>( + owner: &'a AccountInfo, + mint: &'a AccountInfo, + payer: &'a AccountInfo, + ata: &'a AccountInfo, + compressible: &CompressibleParamsCpi<'a>, + idempotent: bool, +) -> Result<(Vec, [AccountMeta<'a>; 7], [&'a AccountInfo; 7]), ProgramError> { + let instruction_data = CreateAssociatedTokenAccountInstructionData { + compressible_config: Some(CompressibleExtensionInstructionData { + token_account_version: compressible.token_account_version as u8, + rent_payment: compressible.pre_pay_num_epochs, + compression_only: compressible.compression_only as u8, + write_top_up: compressible.lamports_per_write.unwrap_or(0), + compress_to_account_pubkey: compressible.compress_to_account_pubkey.clone(), + }), + }; + + let discriminator = if idempotent { + CREATE_ATA_IDEMPOTENT_DISCRIMINATOR + } else { + CREATE_ATA_DISCRIMINATOR + }; + + let mut data = Vec::new(); + data.push(discriminator); + instruction_data + .serialize(&mut data) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + // Account order matches the cToken program: + // [0] owner (readonly) + // [1] mint (readonly) + // [2] payer (signer, writable) + // [3] associated_token_account (writable) + // [4] system_program (readonly) + // [5] compressible_config (readonly) + // [6] rent_sponsor (writable) + let metas = [ + AccountMeta::readonly(owner.key()), + AccountMeta::readonly(mint.key()), + AccountMeta::writable_signer(payer.key()), + AccountMeta::writable(ata.key()), + AccountMeta::readonly(compressible.system_program.key()), + AccountMeta::readonly(compressible.compressible_config.key()), + AccountMeta::writable(compressible.rent_sponsor.key()), + ]; + + let account_infos = [ + owner, + mint, + payer, + ata, + compressible.system_program, + compressible.compressible_config, + compressible.rent_sponsor, + ]; + + Ok((data, metas, account_infos)) +} + +/// Helper to invoke CPI to Light Token program. +fn invoke_cpi( + data: &[u8], + metas: &[AccountMeta], + account_infos: &[&AccountInfo], + signers: &[Signer], +) -> Result<(), ProgramError> { + let instruction = Instruction { + program_id: &LIGHT_TOKEN_PROGRAM_ID, + accounts: metas, + data, + }; + slice_invoke_signed(&instruction, account_infos, signers) +} diff --git a/sdk-libs/token-pinocchio/src/instruction/mod.rs b/sdk-libs/token-pinocchio/src/instruction/mod.rs index 5736746fbd..b27c434a09 100644 --- a/sdk-libs/token-pinocchio/src/instruction/mod.rs +++ b/sdk-libs/token-pinocchio/src/instruction/mod.rs @@ -34,6 +34,7 @@ mod approve; mod burn; mod burn_checked; mod close; +pub mod compressible; mod create; mod create_ata; mod create_mint; @@ -53,6 +54,7 @@ pub use approve::*; pub use burn::*; pub use burn_checked::*; pub use close::*; +pub use compressible::{CompressibleParams, CompressibleParamsCpi}; pub use create::*; pub use create_ata::{ derive_associated_token_account, CreateTokenAtaCpi, CreateTokenAtaCpiIdempotent, diff --git a/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs b/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs index a3df0ccdbf..f913ce95ff 100644 --- a/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs +++ b/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs @@ -12,13 +12,33 @@ use super::{ transfer_checked::TransferCheckedCpi, transfer_from_spl::TransferFromSplCpi, transfer_to_spl::TransferToSplCpi, }; +use crate::error::LightTokenError; /// SPL Token transfer_checked instruction discriminator const SPL_TRANSFER_CHECKED_DISCRIMINATOR: u8 = 12; +/// SPL Token Program ID +const SPL_TOKEN_PROGRAM_ID: [u8; 32] = + light_macros::pubkey_array!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); + +/// SPL Token 2022 Program ID +const SPL_TOKEN_2022_PROGRAM_ID: [u8; 32] = + light_macros::pubkey_array!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"); + /// Check if an account is owned by the Light Token program. -fn is_light_token_owner(owner: &[u8; 32]) -> bool { - owner == &LIGHT_TOKEN_PROGRAM_ID +/// +/// Returns `Ok(true)` for Light Token program, `Ok(false)` for SPL Token / Token-2022, +/// and `Err(CannotDetermineAccountType)` for unrecognized owners. +fn is_light_token_owner(owner: &[u8; 32]) -> Result { + if owner == &LIGHT_TOKEN_PROGRAM_ID { + return Ok(true); + } + + if owner == &SPL_TOKEN_PROGRAM_ID || owner == &SPL_TOKEN_2022_PROGRAM_ID { + return Ok(false); + } + + Err(LightTokenError::CannotDetermineAccountType) } /// Internal enum to classify transfer types based on account owners. @@ -35,15 +55,33 @@ enum TransferType { } /// Determine transfer type from account owners. -fn determine_transfer_type(source_owner: &[u8; 32], destination_owner: &[u8; 32]) -> TransferType { - let source_is_light = is_light_token_owner(source_owner); - let dest_is_light = is_light_token_owner(destination_owner); +/// +/// Returns `Ok(TransferType)` for valid account combinations. +/// Returns `Err(CannotDetermineAccountType)` if an account owner is unrecognized. +/// Returns `Err(SplTokenProgramMismatch)` if both are SPL but with different token programs. +fn determine_transfer_type( + source_owner: &[u8; 32], + destination_owner: &[u8; 32], +) -> Result { + let source_is_light = is_light_token_owner(source_owner) + .map_err(|_| ProgramError::Custom(LightTokenError::CannotDetermineAccountType.into()))?; + let dest_is_light = is_light_token_owner(destination_owner) + .map_err(|_| ProgramError::Custom(LightTokenError::CannotDetermineAccountType.into()))?; match (source_is_light, dest_is_light) { - (true, true) => TransferType::LightToLight, - (true, false) => TransferType::LightToSpl, - (false, true) => TransferType::SplToLight, - (false, false) => TransferType::SplToSpl, + (true, true) => Ok(TransferType::LightToLight), + (true, false) => Ok(TransferType::LightToSpl), + (false, true) => Ok(TransferType::SplToLight), + (false, false) => { + // Both are SPL - verify same token program + if source_owner == destination_owner { + Ok(TransferType::SplToSpl) + } else { + Err(ProgramError::Custom( + LightTokenError::SplTokenProgramMismatch.into(), + )) + } + } } } @@ -128,7 +166,7 @@ impl<'info> TransferInterfaceCpi<'info> { let transfer_type = determine_transfer_type( self.source_account.owner(), self.destination_account.owner(), - ); + )?; match transfer_type { TransferType::LightToLight => TransferCheckedCpi { @@ -144,7 +182,9 @@ impl<'info> TransferInterfaceCpi<'info> { .invoke(), TransferType::LightToSpl => { - let spl = self.spl_interface.ok_or(ProgramError::InvalidAccountData)?; + let spl = self.spl_interface.ok_or(ProgramError::Custom( + LightTokenError::SplInterfaceRequired.into(), + ))?; TransferToSplCpi { source: self.source_account, destination_spl_token_account: self.destination_account, @@ -162,7 +202,9 @@ impl<'info> TransferInterfaceCpi<'info> { } TransferType::SplToLight => { - let spl = self.spl_interface.ok_or(ProgramError::InvalidAccountData)?; + let spl = self.spl_interface.ok_or(ProgramError::Custom( + LightTokenError::SplInterfaceRequired.into(), + ))?; TransferFromSplCpi { amount: self.amount, spl_interface_pda_bump: spl.spl_interface_pda_bump, @@ -182,7 +224,9 @@ impl<'info> TransferInterfaceCpi<'info> { TransferType::SplToSpl => { // For SPL-to-SPL, invoke SPL token program directly via transfer_checked - let spl = self.spl_interface.ok_or(ProgramError::InvalidAccountData)?; + let spl = self.spl_interface.ok_or(ProgramError::Custom( + LightTokenError::SplInterfaceRequired.into(), + ))?; // Build SPL transfer_checked instruction data: [12, amount(8), decimals(1)] let mut ix_data = [0u8; 10]; @@ -226,7 +270,7 @@ impl<'info> TransferInterfaceCpi<'info> { let transfer_type = determine_transfer_type( self.source_account.owner(), self.destination_account.owner(), - ); + )?; match transfer_type { TransferType::LightToLight => TransferCheckedCpi { @@ -242,7 +286,9 @@ impl<'info> TransferInterfaceCpi<'info> { .invoke_signed(signers), TransferType::LightToSpl => { - let spl = self.spl_interface.ok_or(ProgramError::InvalidAccountData)?; + let spl = self.spl_interface.ok_or(ProgramError::Custom( + LightTokenError::SplInterfaceRequired.into(), + ))?; TransferToSplCpi { source: self.source_account, destination_spl_token_account: self.destination_account, @@ -260,7 +306,9 @@ impl<'info> TransferInterfaceCpi<'info> { } TransferType::SplToLight => { - let spl = self.spl_interface.ok_or(ProgramError::InvalidAccountData)?; + let spl = self.spl_interface.ok_or(ProgramError::Custom( + LightTokenError::SplInterfaceRequired.into(), + ))?; TransferFromSplCpi { amount: self.amount, spl_interface_pda_bump: spl.spl_interface_pda_bump, @@ -280,7 +328,9 @@ impl<'info> TransferInterfaceCpi<'info> { TransferType::SplToSpl => { // For SPL-to-SPL, invoke SPL token program directly via transfer_checked - let spl = self.spl_interface.ok_or(ProgramError::InvalidAccountData)?; + let spl = self.spl_interface.ok_or(ProgramError::Custom( + LightTokenError::SplInterfaceRequired.into(), + ))?; // Build SPL transfer_checked instruction data: [12, amount(8), decimals(1)] let mut ix_data = [0u8; 10]; diff --git a/sdk-libs/token-pinocchio/src/lib.rs b/sdk-libs/token-pinocchio/src/lib.rs index d806db0ef6..f1f080db24 100644 --- a/sdk-libs/token-pinocchio/src/lib.rs +++ b/sdk-libs/token-pinocchio/src/lib.rs @@ -22,7 +22,6 @@ //! | Create Token ATA | [`CreateTokenAtaCpi`](instruction::CreateTokenAtaCpi) | //! | Create Mint | [`CreateMintCpi`](instruction::CreateMintCpi) | //! | Create Mints (Batch) | [`CreateMintsCpi`](instruction::CreateMintsCpi) | -//! | Decompress Mint | [`DecompressMintCpi`](instruction::DecompressMintCpi) | //! //! ## Example: Transfer via CPI //! diff --git a/sdk-tests/sdk-light-token-pinocchio/src/create_ata.rs b/sdk-tests/sdk-light-token-pinocchio/src/create_ata.rs index ce41a1deda..0ca72d9975 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/create_ata.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/create_ata.rs @@ -1,6 +1,10 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use light_token_pinocchio::instruction::CreateTokenAtaCpi; -use pinocchio::{account_info::AccountInfo, program_error::ProgramError}; +use light_token_pinocchio::instruction::{CompressibleParamsCpi, CreateTokenAtaCpi}; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, +}; use crate::{ATA_SEED, ID}; @@ -72,7 +76,9 @@ pub fn process_create_ata_invoke_signed( return Err(ProgramError::InvalidSeeds); } - let signer_seeds: &[&[u8]] = &[ATA_SEED, &[bump]]; + let bump_byte = [bump]; + let seeds = [Seed::from(ATA_SEED), Seed::from(&bump_byte[..])]; + let signer = Signer::from(&seeds); CreateTokenAtaCpi { payer: &accounts[2], @@ -85,7 +91,80 @@ pub fn process_create_ata_invoke_signed( &accounts[6], // rent_sponsor &accounts[4], // system_program ) - .invoke_signed(&[signer_seeds]) + .invoke_signed(&[signer]) + .map_err(|_| ProgramError::Custom(0))?; + + Ok(()) +} + +/// Handler for creating a compressible ATA using invoke_with (explicit CompressibleParamsCpi). +/// +/// Account order: +/// - accounts[0]: owner +/// - accounts[1]: mint +/// - accounts[2]: payer (signer) +/// - accounts[3]: associated token account (derived) +/// - accounts[4]: system_program +/// - accounts[5]: compressible_config +/// - accounts[6]: rent_sponsor +pub fn process_create_ata_invoke_with( + accounts: &[AccountInfo], + _data: CreateAtaData, +) -> Result<(), ProgramError> { + if accounts.len() < 7 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + let compressible = CompressibleParamsCpi::new_ata( + &accounts[5], // compressible_config + &accounts[6], // rent_sponsor + &accounts[4], // system_program + ); + + CreateTokenAtaCpi { + payer: &accounts[2], + owner: &accounts[0], + mint: &accounts[1], + ata: &accounts[3], + } + .invoke_with(compressible) + .map_err(|_| ProgramError::Custom(0))?; + + Ok(()) +} + +/// Handler for creating a compressible ATA idempotently using idempotent().invoke_with(). +/// +/// Account order: +/// - accounts[0]: owner +/// - accounts[1]: mint +/// - accounts[2]: payer (signer) +/// - accounts[3]: associated token account (derived) +/// - accounts[4]: system_program +/// - accounts[5]: compressible_config +/// - accounts[6]: rent_sponsor +pub fn process_create_ata_idempotent_invoke_with( + accounts: &[AccountInfo], + _data: CreateAtaData, +) -> Result<(), ProgramError> { + if accounts.len() < 7 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + let compressible = CompressibleParamsCpi::new_ata( + &accounts[5], // compressible_config + &accounts[6], // rent_sponsor + &accounts[4], // system_program + ); + + CreateTokenAtaCpi { + payer: &accounts[2], + owner: &accounts[0], + mint: &accounts[1], + ata: &accounts[3], + } + .idempotent() + .invoke_with(compressible) .map_err(|_| ProgramError::Custom(0))?; Ok(()) diff --git a/sdk-tests/sdk-light-token-pinocchio/src/create_token_account.rs b/sdk-tests/sdk-light-token-pinocchio/src/create_token_account.rs index 27d157020a..f8f0f28a5a 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/create_token_account.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/create_token_account.rs @@ -1,6 +1,10 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use light_token_pinocchio::instruction::CreateTokenAccountCpi; -use pinocchio::{account_info::AccountInfo, program_error::ProgramError}; +use light_token_pinocchio::instruction::{CompressibleParamsCpi, CreateTokenAccountCpi}; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, +}; use crate::{ID, TOKEN_ACCOUNT_SEED}; @@ -44,7 +48,6 @@ pub fn process_create_token_account_invoke( &accounts[3], // compressible_config &accounts[5], // rent_sponsor &accounts[4], // system_program - &ID, ) .invoke() .map_err(|_| ProgramError::Custom(0))?; @@ -78,7 +81,9 @@ pub fn process_create_token_account_invoke_signed( } // Invoke with PDA signing and rent-free config - let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]]; + let bump_byte = [bump]; + let seeds = [Seed::from(TOKEN_ACCOUNT_SEED), Seed::from(&bump_byte[..])]; + let signer = Signer::from(&seeds); CreateTokenAccountCpi { payer: &accounts[0], account: &accounts[1], @@ -89,9 +94,88 @@ pub fn process_create_token_account_invoke_signed( &accounts[3], // compressible_config &accounts[5], // rent_sponsor &accounts[4], // system_program - &ID, ) - .invoke_signed(signer_seeds) + .invoke_signed(&[signer]) + .map_err(|_| ProgramError::Custom(0))?; + + Ok(()) +} + +/// Handler for creating a compressible token account using invoke_with (explicit CompressibleParamsCpi). +/// +/// Account order: +/// - accounts[0]: payer (signer) +/// - accounts[1]: account to create (signer) +/// - accounts[2]: mint +/// - accounts[3]: compressible_config +/// - accounts[4]: system_program +/// - accounts[5]: rent_sponsor +pub fn process_create_token_account_invoke_with( + accounts: &[AccountInfo], + data: CreateTokenAccountData, +) -> Result<(), ProgramError> { + if accounts.len() < 6 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + let compressible = CompressibleParamsCpi::new( + &accounts[3], // compressible_config + &accounts[5], // rent_sponsor + &accounts[4], // system_program + ); + + CreateTokenAccountCpi { + payer: &accounts[0], + account: &accounts[1], + mint: &accounts[2], + owner: data.owner, + } + .invoke_with(compressible) + .map_err(|_| ProgramError::Custom(0))?; + + Ok(()) +} + +/// Handler for creating a PDA-owned compressible token account using invoke_signed_with. +/// +/// Account order: +/// - accounts[0]: payer (signer) +/// - accounts[1]: account to create (PDA, will be derived and verified) +/// - accounts[2]: mint +/// - accounts[3]: compressible_config +/// - accounts[4]: system_program +/// - accounts[5]: rent_sponsor +pub fn process_create_token_account_invoke_signed_with( + accounts: &[AccountInfo], + data: CreateTokenAccountData, +) -> Result<(), ProgramError> { + if accounts.len() < 6 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + let (pda, bump) = pinocchio::pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &ID); + + if pda != *accounts[1].key() { + return Err(ProgramError::InvalidSeeds); + } + + let bump_byte = [bump]; + let seeds = [Seed::from(TOKEN_ACCOUNT_SEED), Seed::from(&bump_byte[..])]; + let signer = Signer::from(&seeds); + + let compressible = CompressibleParamsCpi::new( + &accounts[3], // compressible_config + &accounts[5], // rent_sponsor + &accounts[4], // system_program + ); + + CreateTokenAccountCpi { + payer: &accounts[0], + account: &accounts[1], + mint: &accounts[2], + owner: data.owner, + } + .invoke_signed_with(compressible, &[signer]) .map_err(|_| ProgramError::Custom(0))?; Ok(()) diff --git a/sdk-tests/sdk-light-token-pinocchio/src/lib.rs b/sdk-tests/sdk-light-token-pinocchio/src/lib.rs index 054b4da2fa..d653123ac8 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/lib.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/lib.rs @@ -27,13 +27,17 @@ pub use burn::{ process_burn_invoke, process_burn_invoke_signed, process_burn_invoke_with_fee_payer, BurnData, }; pub use close::{process_close_account_invoke, process_close_account_invoke_signed}; -pub use create_ata::{process_create_ata_invoke, process_create_ata_invoke_signed, CreateAtaData}; +pub use create_ata::{ + process_create_ata_idempotent_invoke_with, process_create_ata_invoke, + process_create_ata_invoke_signed, process_create_ata_invoke_with, CreateAtaData, +}; pub use create_mint::{ process_create_mint, process_create_mint_invoke_signed, process_create_mint_with_pda_authority, CreateCmintData, MINT_SIGNER_SEED, }; pub use create_token_account::{ process_create_token_account_invoke, process_create_token_account_invoke_signed, + process_create_token_account_invoke_signed_with, process_create_token_account_invoke_with, CreateTokenAccountData, }; pub use ctoken_mint_to::{ @@ -157,6 +161,14 @@ pub enum InstructionType { ApproveInvokeWithFeePayer = 39, /// Revoke delegation with separate fee_payer (invoke, non-PDA authority) RevokeInvokeWithFeePayer = 40, + /// Create compressible token account using invoke_with (explicit CompressibleParamsCpi) + CreateTokenAccountInvokeWith = 41, + /// Create compressible token account using invoke_signed_with (explicit CompressibleParamsCpi) + CreateTokenAccountInvokeSignedWith = 42, + /// Create compressible ATA using invoke_with (explicit CompressibleParamsCpi) + CreateAtaInvokeWith = 43, + /// Create compressible ATA idempotently using idempotent().invoke_with() + CreateAtaIdempotentInvokeWith = 44, } impl TryFrom for InstructionType { @@ -202,6 +214,10 @@ impl TryFrom for InstructionType { 38 => Ok(InstructionType::CTokenMintToInvokeWithFeePayer), 39 => Ok(InstructionType::ApproveInvokeWithFeePayer), 40 => Ok(InstructionType::RevokeInvokeWithFeePayer), + 41 => Ok(InstructionType::CreateTokenAccountInvokeWith), + 42 => Ok(InstructionType::CreateTokenAccountInvokeSignedWith), + 43 => Ok(InstructionType::CreateAtaInvokeWith), + 44 => Ok(InstructionType::CreateAtaIdempotentInvokeWith), _ => Err(ProgramError::InvalidInstructionData), } } @@ -370,6 +386,26 @@ pub fn process_instruction( process_approve_invoke_with_fee_payer(accounts, data) } InstructionType::RevokeInvokeWithFeePayer => process_revoke_invoke_with_fee_payer(accounts), + InstructionType::CreateTokenAccountInvokeWith => { + let data = CreateTokenAccountData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_create_token_account_invoke_with(accounts, data) + } + InstructionType::CreateTokenAccountInvokeSignedWith => { + let data = CreateTokenAccountData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_create_token_account_invoke_signed_with(accounts, data) + } + InstructionType::CreateAtaInvokeWith => { + let data = CreateAtaData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_create_ata_invoke_with(accounts, data) + } + InstructionType::CreateAtaIdempotentInvokeWith => { + let data = CreateAtaData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_create_ata_idempotent_invoke_with(accounts, data) + } _ => Err(ProgramError::InvalidInstructionData), } } diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_create_ata.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_create_ata.rs index f7d6eae111..291d180ba6 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/test_create_ata.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_create_ata.rs @@ -6,6 +6,9 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_client::rpc::Rpc; use light_program_test::{LightProgramTest, ProgramTestConfig}; use light_token::instruction::LIGHT_TOKEN_PROGRAM_ID; +use light_token_interface::state::{ + AccountState, ExtensionStruct, Token, ACCOUNT_TYPE_TOKEN_ACCOUNT, +}; use sdk_light_token_pinocchio_test::{CreateAtaData, ATA_SEED}; use shared::*; use solana_sdk::{ @@ -14,6 +17,34 @@ use solana_sdk::{ signer::Signer, }; +fn assert_ata_account(account_state: &Token, mint_pda: Pubkey, owner: Pubkey) { + let compressible_ext = account_state + .extensions + .as_ref() + .and_then(|exts| { + exts.iter().find_map(|e| match e { + ExtensionStruct::Compressible(info) => Some(*info), + _ => None, + }) + }) + .expect("ATA should have Compressible extension"); + + let expected = Token { + mint: mint_pda.to_bytes().into(), + owner: owner.to_bytes().into(), + amount: 0, + delegate: None, + state: AccountState::Initialized, + is_native: None, + delegated_amount: 0, + close_authority: None, + account_type: ACCOUNT_TYPE_TOKEN_ACCOUNT, + extensions: Some(vec![ExtensionStruct::Compressible(compressible_ext)]), + }; + + assert_eq!(account_state, &expected); +} + /// Test creating an ATA using CreateAssociatedTokenAccountCpi::invoke() #[tokio::test] async fn test_create_ata_invoke() { @@ -27,16 +58,13 @@ async fn test_create_ata_invoke() { let payer = rpc.get_payer().insecure_clone(); let mint_authority = payer.pubkey(); - // Create compressed mint first (using helper) let (mint_pda, _compression_address, _, _mint_seed) = setup_create_mint(&mut rpc, &payer, mint_authority, 9, vec![]).await; - // Derive the ATA address let owner = payer.pubkey(); use light_token::instruction::derive_token_ata; let ata_address = derive_token_ata(&owner, &mint_pda); - // Build CreateAtaData (owner and mint are passed as accounts) let create_ata_data = CreateAtaData { pre_pay_num_epochs: 2, lamports_per_write: 1, @@ -48,7 +76,6 @@ async fn test_create_ata_invoke() { let config = config_pda(); let rent_sponsor = rent_sponsor_pda(); - // Account order: owner, mint, payer, ata, system_program, config, rent_sponsor, light_token_program let instruction = Instruction { program_id: PROGRAM_ID, accounts: vec![ @@ -68,23 +95,10 @@ async fn test_create_ata_invoke() { .await .unwrap(); - // Verify ATA was created let ata_account_data = rpc.get_account(ata_address).await.unwrap().unwrap(); - // Parse and verify account data - use light_token_interface::state::Token; let account_state = Token::deserialize(&mut &ata_account_data.data[..]).unwrap(); - assert_eq!( - account_state.mint.to_bytes(), - mint_pda.to_bytes(), - "Mint should match" - ); - assert_eq!( - account_state.owner.to_bytes(), - owner.to_bytes(), - "Owner should match" - ); - assert_eq!(account_state.amount, 0, "Initial amount should be 0"); + assert_ata_account(&account_state, mint_pda, owner); } /// Test creating an ATA with PDA payer using CreateAssociatedTokenAccountCpi::invoke_signed() @@ -100,11 +114,9 @@ async fn test_create_ata_invoke_signed() { let payer = rpc.get_payer().insecure_clone(); let mint_authority = payer.pubkey(); - // Create compressed mint first (using helper) let (mint_pda, _compression_address, _, _mint_seed) = setup_create_mint(&mut rpc, &payer, mint_authority, 9, vec![]).await; - // Derive the PDA that will act as payer/owner (using ATA_SEED) let (pda_owner, _pda_bump) = Pubkey::find_program_address(&[ATA_SEED], &PROGRAM_ID); // Fund the PDA so it can pay for the ATA creation @@ -117,11 +129,9 @@ async fn test_create_ata_invoke_signed() { .await .unwrap(); - // Derive the ATA address for the PDA owner use light_token::instruction::derive_token_ata; let ata_address = derive_token_ata(&pda_owner, &mint_pda); - // Build CreateAtaData with PDA as owner (owner and mint are passed as accounts) let create_ata_data = CreateAtaData { pre_pay_num_epochs: 2, lamports_per_write: 1, @@ -133,7 +143,6 @@ async fn test_create_ata_invoke_signed() { let config = config_pda(); let rent_sponsor = rent_sponsor_pda(); - // Account order: owner, mint, payer, ata, system_program, config, rent_sponsor, light_token_program let instruction = Instruction { program_id: PROGRAM_ID, accounts: vec![ @@ -153,21 +162,120 @@ async fn test_create_ata_invoke_signed() { .await .unwrap(); - // Verify ATA was created let ata_account_data = rpc.get_account(ata_address).await.unwrap().unwrap(); - // Parse and verify account data - use light_token_interface::state::Token; let account_state = Token::deserialize(&mut &ata_account_data.data[..]).unwrap(); - assert_eq!( - account_state.mint.to_bytes(), - mint_pda.to_bytes(), - "Mint should match" - ); - assert_eq!( - account_state.owner.to_bytes(), - pda_owner.to_bytes(), - "Owner should match PDA" - ); - assert_eq!(account_state.amount, 0, "Initial amount should be 0"); + assert_ata_account(&account_state, mint_pda, pda_owner); +} + +/// Test creating an ATA using CreateAssociatedTokenAccountCpi::invoke_with() +#[tokio::test] +async fn test_create_ata_invoke_with() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let mint_authority = payer.pubkey(); + + let (mint_pda, _compression_address, _, _mint_seed) = + setup_create_mint(&mut rpc, &payer, mint_authority, 9, vec![]).await; + + let owner = payer.pubkey(); + use light_token::instruction::derive_token_ata; + let ata_address = derive_token_ata(&owner, &mint_pda); + + let create_ata_data = CreateAtaData { + pre_pay_num_epochs: 2, + lamports_per_write: 1, + }; + // Discriminator 43 = CreateAtaInvokeWith + let instruction_data = [vec![43u8], create_ata_data.try_to_vec().unwrap()].concat(); + + use light_token::instruction::{config_pda, rent_sponsor_pda}; + let config = config_pda(); + let rent_sponsor = rent_sponsor_pda(); + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new_readonly(owner, false), + AccountMeta::new_readonly(mint_pda, false), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(ata_address, false), + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(config, false), + AccountMeta::new(rent_sponsor, false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + let ata_account_data = rpc.get_account(ata_address).await.unwrap().unwrap(); + + let account_state = Token::deserialize(&mut &ata_account_data.data[..]).unwrap(); + assert_ata_account(&account_state, mint_pda, owner); +} + +/// Test creating an ATA idempotently using CreateAssociatedTokenAccountCpi::idempotent().invoke_with() +#[tokio::test] +async fn test_create_ata_idempotent_invoke_with() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let mint_authority = payer.pubkey(); + + let (mint_pda, _compression_address, _, _mint_seed) = + setup_create_mint(&mut rpc, &payer, mint_authority, 9, vec![]).await; + + let owner = payer.pubkey(); + use light_token::instruction::derive_token_ata; + let ata_address = derive_token_ata(&owner, &mint_pda); + + let create_ata_data = CreateAtaData { + pre_pay_num_epochs: 2, + lamports_per_write: 1, + }; + // Discriminator 44 = CreateAtaIdempotentInvokeWith + let instruction_data = [vec![44u8], create_ata_data.try_to_vec().unwrap()].concat(); + + use light_token::instruction::{config_pda, rent_sponsor_pda}; + let config = config_pda(); + let rent_sponsor = rent_sponsor_pda(); + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new_readonly(owner, false), + AccountMeta::new_readonly(mint_pda, false), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(ata_address, false), + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(config, false), + AccountMeta::new(rent_sponsor, false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + let ata_account_data = rpc.get_account(ata_address).await.unwrap().unwrap(); + + let account_state = Token::deserialize(&mut &ata_account_data.data[..]).unwrap(); + assert_ata_account(&account_state, mint_pda, owner); } diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_create_token_account.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_create_token_account.rs index 2bd383f880..4f86edba8d 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/test_create_token_account.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_create_token_account.rs @@ -6,7 +6,10 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_client::rpc::Rpc; use light_program_test::{LightProgramTest, ProgramTestConfig}; use light_token::instruction::LIGHT_TOKEN_PROGRAM_ID; -use sdk_light_token_pinocchio_test::CreateTokenAccountData; +use light_token_interface::state::{ + AccountState, ExtensionStruct, Token, ACCOUNT_TYPE_TOKEN_ACCOUNT, +}; +use sdk_light_token_pinocchio_test::{CreateTokenAccountData, TOKEN_ACCOUNT_SEED}; use shared::*; use solana_sdk::{ instruction::{AccountMeta, Instruction}, @@ -15,6 +18,34 @@ use solana_sdk::{ signer::Signer, }; +fn assert_token_account(account_state: &Token, mint_pda: Pubkey, owner: Pubkey) { + let compressible_ext = account_state + .extensions + .as_ref() + .and_then(|exts| { + exts.iter().find_map(|e| match e { + ExtensionStruct::Compressible(info) => Some(*info), + _ => None, + }) + }) + .expect("Token account should have Compressible extension"); + + let expected = Token { + mint: mint_pda.to_bytes().into(), + owner: owner.to_bytes().into(), + amount: 0, + delegate: None, + state: AccountState::Initialized, + is_native: None, + delegated_amount: 0, + close_authority: None, + account_type: ACCOUNT_TYPE_TOKEN_ACCOUNT, + extensions: Some(vec![ExtensionStruct::Compressible(compressible_ext)]), + }; + + assert_eq!(account_state, &expected); +} + /// Test creating a token account using CreateTokenAccountCpi::invoke() #[tokio::test] async fn test_create_token_account_invoke() { @@ -28,11 +59,9 @@ async fn test_create_token_account_invoke() { let payer = rpc.get_payer().insecure_clone(); let mint_authority = payer.pubkey(); - // Create compressed mint first (using helper) let (mint_pda, _compression_address, _, _mint_seed) = setup_create_mint(&mut rpc, &payer, mint_authority, 9, vec![]).await; - // Create ctoken account via wrapper program let ctoken_account = Keypair::new(); let owner = payer.pubkey(); @@ -41,6 +70,7 @@ async fn test_create_token_account_invoke() { pre_pay_num_epochs: 2, lamports_per_write: 1, }; + // Discriminator 2 = CreateTokenAccountInvoke let instruction_data = [vec![2u8], create_token_account_data.try_to_vec().unwrap()].concat(); use light_token::instruction::{config_pda, rent_sponsor_pda}; @@ -65,27 +95,14 @@ async fn test_create_token_account_invoke() { .await .unwrap(); - // Verify ctoken account was created let ctoken_account_data = rpc .get_account(ctoken_account.pubkey()) .await .unwrap() .unwrap(); - // Parse and verify account data - use light_token_interface::state::Token; let account_state = Token::deserialize(&mut &ctoken_account_data.data[..]).unwrap(); - assert_eq!( - account_state.mint.to_bytes(), - mint_pda.to_bytes(), - "Mint should match" - ); - assert_eq!( - account_state.owner.to_bytes(), - owner.to_bytes(), - "Owner should match" - ); - assert_eq!(account_state.amount, 0, "Initial amount should be 0"); + assert_token_account(&account_state, mint_pda, owner); } /// Test creating a PDA-owned token account using CreateTokenAccountCpi::invoke_signed() @@ -101,11 +118,9 @@ async fn test_create_token_account_invoke_signed() { let payer = rpc.get_payer().insecure_clone(); let mint_authority = payer.pubkey(); - // Create compressed mint first (using helper) let (mint_pda, _compression_address, _, _mint_seed) = setup_create_mint(&mut rpc, &payer, mint_authority, 9, vec![]).await; - // Derive the PDA for the token account (same seeds as in the program) let token_account_seed: &[u8] = b"token_account"; let (ctoken_account_pda, _bump) = Pubkey::find_program_address(&[token_account_seed], &PROGRAM_ID); @@ -138,26 +153,130 @@ async fn test_create_token_account_invoke_signed() { data: instruction_data, }; - // Note: only payer signs, the PDA account is signed by the program via invoke_signed + // Only payer signs; PDA is signed by the program via invoke_signed + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + let ctoken_account_data = rpc.get_account(ctoken_account_pda).await.unwrap().unwrap(); + + let account_state = Token::deserialize(&mut &ctoken_account_data.data[..]).unwrap(); + assert_token_account(&account_state, mint_pda, owner); +} + +/// Test creating a token account using CreateTokenAccountCpi::invoke_with() +#[tokio::test] +async fn test_create_token_account_invoke_with() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let mint_authority = payer.pubkey(); + + let (mint_pda, _compression_address, _, _mint_seed) = + setup_create_mint(&mut rpc, &payer, mint_authority, 9, vec![]).await; + + let ctoken_account = Keypair::new(); + let owner = payer.pubkey(); + + let create_token_account_data = CreateTokenAccountData { + owner: owner.to_bytes(), + pre_pay_num_epochs: 2, + lamports_per_write: 1, + }; + // Discriminator 41 = CreateTokenAccountInvokeWith + let instruction_data = [vec![41u8], create_token_account_data.try_to_vec().unwrap()].concat(); + + use light_token::instruction::{config_pda, rent_sponsor_pda}; + let config = config_pda(); + let rent_sponsor = rent_sponsor_pda(); + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(ctoken_account.pubkey(), true), + AccountMeta::new_readonly(mint_pda, false), + AccountMeta::new_readonly(config, false), + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(rent_sponsor, false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &ctoken_account]) + .await + .unwrap(); + + let ctoken_account_data = rpc + .get_account(ctoken_account.pubkey()) + .await + .unwrap() + .unwrap(); + + let account_state = Token::deserialize(&mut &ctoken_account_data.data[..]).unwrap(); + assert_token_account(&account_state, mint_pda, owner); +} + +/// Test creating a PDA-owned token account using CreateTokenAccountCpi::invoke_signed_with() +#[tokio::test] +async fn test_create_token_account_invoke_signed_with() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let mint_authority = payer.pubkey(); + + let (mint_pda, _compression_address, _, _mint_seed) = + setup_create_mint(&mut rpc, &payer, mint_authority, 9, vec![]).await; + + let (ctoken_account_pda, _bump) = + Pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &PROGRAM_ID); + + let owner = payer.pubkey(); + + let create_token_account_data = CreateTokenAccountData { + owner: owner.to_bytes(), + pre_pay_num_epochs: 2, + lamports_per_write: 1, + }; + // Discriminator 42 = CreateTokenAccountInvokeSignedWith + let instruction_data = [vec![42u8], create_token_account_data.try_to_vec().unwrap()].concat(); + + use light_token::instruction::{config_pda, rent_sponsor_pda}; + let config = config_pda(); + let rent_sponsor = rent_sponsor_pda(); + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(ctoken_account_pda, false), // PDA, not a signer + AccountMeta::new_readonly(mint_pda, false), + AccountMeta::new_readonly(config, false), + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(rent_sponsor, false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + ], + data: instruction_data, + }; + + // Only payer signs; PDA is signed by the program via invoke_signed rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) .await .unwrap(); - // Verify ctoken account was created let ctoken_account_data = rpc.get_account(ctoken_account_pda).await.unwrap().unwrap(); - // Parse and verify account data - use light_token_interface::state::Token; let account_state = Token::deserialize(&mut &ctoken_account_data.data[..]).unwrap(); - assert_eq!( - account_state.mint.to_bytes(), - mint_pda.to_bytes(), - "Mint should match" - ); - assert_eq!( - account_state.owner.to_bytes(), - owner.to_bytes(), - "Owner should match" - ); - assert_eq!(account_state.amount, 0, "Initial amount should be 0"); + assert_token_account(&account_state, mint_pda, owner); }