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 4ad447582d..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,8 +47,7 @@ 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, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -79,8 +78,7 @@ 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, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -130,8 +128,7 @@ 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, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -161,8 +158,7 @@ async fn test_burn_fails() { mint: ctx.mint_pda, amount: 50, authority: ctx.owner_keypair.pubkey(), - max_top_up: None, - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -208,8 +204,7 @@ async fn test_burn_fails() { mint: ctx.mint_pda, amount: 50, authority: ctx.owner_keypair.pubkey(), - max_top_up: None, - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -239,8 +234,7 @@ 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, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -275,8 +269,7 @@ async fn test_burn_fails() { mint: ctx.mint_pda, amount: 50, authority: wrong_authority.pubkey(), - max_top_up: None, - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -379,8 +372,7 @@ async fn setup_burn_test() -> BurnTestContext { destination: ctoken_ata, amount: 100, authority: mint_authority.pubkey(), - max_top_up: None, - fee_payer: None, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); @@ -425,8 +417,7 @@ 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, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -458,8 +449,7 @@ 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, + 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 7b70820bc4..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,8 +195,7 @@ 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, + fee_payer: context.payer.pubkey(), } .instruction() .unwrap(); @@ -243,8 +242,7 @@ 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, + fee_payer: context.payer.pubkey(), } .instruction() .unwrap(); @@ -292,8 +290,7 @@ 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, + 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 a5346b9d14..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,8 +860,7 @@ async fn transfer_checked_and_assert( amount, decimals, authority: authority.pubkey(), - max_top_up: None, - fee_payer: None, + fee_payer: context.payer.pubkey(), } .instruction() .unwrap(); @@ -902,8 +901,7 @@ async fn transfer_checked_and_assert_fails( amount, decimals, authority: authority.pubkey(), - max_top_up: None, - fee_payer: None, + fee_payer: context.payer.pubkey(), } .instruction() .unwrap(); @@ -1075,9 +1073,9 @@ async fn test_ctoken_transfer_checked_max_top_up_exceeded() { amount: 100, decimals: 9, authority: owner_keypair.pubkey(), - max_top_up: Some(1), - fee_payer: None, + fee_payer: context.payer.pubkey(), } + .with_max_top_up(1) .instruction() .unwrap(); 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..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,8 +162,7 @@ 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, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); @@ -186,8 +185,7 @@ 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, + 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 37a73a9447..1811c60784 100644 --- a/program-tests/compressed-token-test/tests/mint/burn.rs +++ b/program-tests/compressed-token-test/tests/mint/burn.rs @@ -118,8 +118,7 @@ async fn test_ctoken_burn() { mint: ctx.mint_pda, amount: 500, authority: ctx.owner_keypair.pubkey(), - max_top_up: None, - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -141,8 +140,7 @@ async fn test_ctoken_burn() { mint: ctx.mint_pda, amount: 500, authority: ctx.owner_keypair.pubkey(), - max_top_up: None, - 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 88efbc2afd..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,8 +96,7 @@ async fn test_ctoken_mint_to() { destination: ctx.ctoken_account, amount: 500, authority: ctx.mint_authority.pubkey(), - max_top_up: None, - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -119,8 +118,7 @@ async fn test_ctoken_mint_to() { destination: ctx.ctoken_account, amount: 500, authority: ctx.mint_authority.pubkey(), - max_top_up: None, - fee_payer: None, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -171,8 +169,7 @@ 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, + fee_payer: ctx.payer.pubkey(), } .instruction() .unwrap(); @@ -214,8 +211,7 @@ 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, + 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 645e40c468..9195fe76f0 100644 --- a/program-tests/registry-test/tests/compressible.rs +++ b/program-tests/registry-test/tests/compressible.rs @@ -1253,8 +1253,7 @@ async fn mint_to_token( destination, amount, authority: mint_authority.pubkey(), - max_top_up: None, - 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/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-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 af7d76403f..0782b28e49 100644 --- a/sdk-libs/token-client/src/actions/mint_to.rs +++ b/sdk-libs/token-client/src/actions/mint_to.rs @@ -46,20 +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(), - max_top_up: None, - 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..a82d0dc5c1 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)))?; @@ -86,15 +87,30 @@ 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(), + 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 de6089df55..9ab8ae9e43 100644 --- a/sdk-libs/token-client/src/actions/transfer.rs +++ b/sdk-libs/token-client/src/actions/transfer.rs @@ -46,20 +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(), - max_top_up: None, - 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 0a5e0f3d7b..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,8 +62,7 @@ impl TransferChecked { amount: self.amount, decimals: self.decimals, authority: authority.pubkey(), - max_top_up: None, - 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_interface.rs b/sdk-libs/token-client/src/actions/transfer_interface.rs index 9ecaf9ca1b..a76ddaeb2d 100644 --- a/sdk-libs/token-client/src/actions/transfer_interface.rs +++ b/sdk-libs/token-client/src/actions/transfer_interface.rs @@ -103,8 +103,8 @@ impl TransferInterface { decimals: self.decimals, authority: authority.pubkey(), payer: payer.pubkey(), + mint: self.mint, 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 af6f821271..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 /// @@ -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,8 @@ pub struct ApproveCpi<'info> { pub owner: &'info AccountInfo, pub system_program: &'info AccountInfo, pub amount: u64, + /// Fee payer for rent top-ups. + pub fee_payer: &'info AccountInfo, } impl<'info> ApproveCpi<'info> { @@ -50,8 +53,9 @@ 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()), ]; let instruction = Instruction { @@ -65,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/burn.rs b/sdk-libs/token-pinocchio/src/instruction/burn.rs index 2bc9f423b0..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 /// @@ -23,8 +23,7 @@ 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, +/// fee_payer: &ctx.accounts.fee_payer, /// } /// .invoke()?; /// ``` @@ -34,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. + pub fee_payer: &'info AccountInfo, } impl<'info> BurnCpi<'info> { @@ -44,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 4793aaf1a8..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 /// @@ -24,8 +24,7 @@ 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, +/// fee_payer: &ctx.accounts.fee_payer, /// } /// .invoke()?; /// ``` @@ -36,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. + pub fee_payer: &'info AccountInfo, } impl<'info> BurnCheckedCpi<'info> { @@ -46,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/close.rs b/sdk-libs/token-pinocchio/src/instruction/close.rs index 66922f9d22..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 /// @@ -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/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 1b4ba5d5cc..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 CToken ATA CPI builder for pinocchio. -//! -//! Re-exports the generic `CreateTokenAtaCpi` from `light_sdk_types` -//! specialized for pinocchio's `AccountInfo`. +//! Create light-token associated token account CPI builder for pinocchio. +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/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 7ca415540a..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 /// @@ -23,8 +23,7 @@ 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, +/// fee_payer: &ctx.accounts.fee_payer, /// } /// .invoke()?; /// ``` @@ -34,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. + pub fee_payer: &'info AccountInfo, } impl<'info> MintToCpi<'info> { @@ -44,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 38e0dd8688..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 /// @@ -24,8 +24,7 @@ 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, +/// fee_payer: &ctx.accounts.fee_payer, /// } /// .invoke()?; /// ``` @@ -36,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. + pub fee_payer: &'info AccountInfo, } impl<'info> MintToCheckedCpi<'info> { @@ -46,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/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/revoke.rs b/sdk-libs/token-pinocchio/src/instruction/revoke.rs index 1cbca61334..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 /// @@ -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,8 @@ pub struct RevokeCpi<'info> { pub token_account: &'info AccountInfo, pub owner: &'info AccountInfo, pub system_program: &'info AccountInfo, + /// Fee payer for rent top-ups. + pub fee_payer: &'info AccountInfo, } impl<'info> RevokeCpi<'info> { @@ -43,8 +46,9 @@ 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()), ]; let instruction = Instruction { @@ -53,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-pinocchio/src/instruction/transfer.rs b/sdk-libs/token-pinocchio/src/instruction/transfer.rs index b27b3ff013..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 /// @@ -23,8 +23,7 @@ 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, +/// fee_payer: &ctx.accounts.fee_payer, /// } /// .invoke()?; /// ``` @@ -34,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. + pub fee_payer: &'info AccountInfo, } impl<'info> TransferCpi<'info> { @@ -44,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_checked.rs b/sdk-libs/token-pinocchio/src/instruction/transfer_checked.rs index f3b7e8f229..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 /// @@ -25,7 +25,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 +37,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. + pub fee_payer: &'info AccountInfo, } impl<'info> TransferCheckedCpi<'info> { @@ -47,84 +47,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(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.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-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_interface.rs b/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs index 7fe0f4e2d7..f913ce95ff 100644 --- a/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs +++ b/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs @@ -9,15 +9,36 @@ 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, }; +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. @@ -34,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(), + )) + } + } } } @@ -69,6 +108,7 @@ pub struct SplInterfaceCpi<'info> { /// &authority, /// &payer, /// &compressed_token_program_authority, +/// &mint, /// &system_program, /// ) /// .invoke()?; @@ -81,6 +121,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 +138,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 +149,7 @@ impl<'info> TransferInterfaceCpi<'info> { authority, payer, compressed_token_program_authority, + mint, spl_interface: None, system_program, } @@ -123,21 +166,25 @@ impl<'info> TransferInterfaceCpi<'info> { let transfer_type = determine_transfer_type( self.source_account.owner(), self.destination_account.owner(), - ); + )?; 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: Some(self.payer), + fee_payer: self.payer, } .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, @@ -155,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, @@ -175,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]; @@ -219,21 +270,25 @@ impl<'info> TransferInterfaceCpi<'info> { let transfer_type = determine_transfer_type( self.source_account.owner(), self.destination_account.owner(), - ); + )?; 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: Some(self.payer), + fee_payer: self.payer, } .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, @@ -251,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, @@ -271,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/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-libs/token-pinocchio/src/lib.rs b/sdk-libs/token-pinocchio/src/lib.rs index 8b772649a1..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 //! @@ -35,8 +34,7 @@ //! amount: 100, //! authority: &ctx.accounts.authority, //! system_program: &ctx.accounts.system_program, -//! max_top_up: None, -//! fee_payer: None, +//! fee_payer: &ctx.accounts.fee_payer, //! } //! .invoke()?; //! ``` 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/instruction/approve.rs b/sdk-libs/token-sdk/src/instruction/approve.rs index b1c6a8cff1..7651554177 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,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 (readonly signer) pub owner: Pubkey, /// Amount of tokens to delegate pub amount: u64, + /// Fee payer for rent top-ups. + pub fee_payer: Pubkey, } /// # Approve Light Token via CPI: @@ -39,12 +43,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 +61,8 @@ pub struct ApproveCpi<'info> { pub owner: AccountInfo<'info>, pub system_program: AccountInfo<'info>, pub amount: u64, + /// Fee payer for rent top-ups. + pub fee_payer: AccountInfo<'info>, } impl<'info> ApproveCpi<'info> { @@ -69,6 +77,7 @@ impl<'info> ApproveCpi<'info> { self.delegate, self.owner, self.system_program, + self.fee_payer, ]; invoke(&instruction, &account_infos) } @@ -80,6 +89,7 @@ impl<'info> ApproveCpi<'info> { self.delegate, self.owner, self.system_program, + self.fee_payer, ]; invoke_signed(&instruction, &account_infos, signer_seeds) } @@ -92,6 +102,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, } } } @@ -106,8 +117,9 @@ 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), ], data, }) diff --git a/sdk-libs/token-sdk/src/instruction/burn.rs b/sdk-libs/token-sdk/src/instruction/burn.rs index f7300e2867..f65287fe4f 100644 --- a/sdk-libs/token-sdk/src/instruction/burn.rs +++ b/sdk-libs/token-sdk/src/instruction/burn.rs @@ -12,13 +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, -/// max_top_up: None, -/// fee_payer: None, +/// fee_payer, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` @@ -31,11 +31,8 @@ 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, + /// Fee payer for rent top-ups. + pub fee_payer: Pubkey, } /// # Burn ctoken via CPI: @@ -46,14 +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, -/// max_top_up: None, -/// fee_payer: None, +/// fee_payer, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -64,10 +61,8 @@ 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>, + /// Fee payer for rent top-ups. + pub fee_payer: AccountInfo<'info>, } impl<'info> BurnCpi<'info> { @@ -77,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) } } @@ -117,47 +102,33 @@ 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), + fee_payer: *cpi.fee_payer.key, } } } -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() { - AccountMeta::new(self.authority, true) - } else { - AccountMeta::new_readonly(self.authority, true) - }; +impl_with_top_up!(Burn, BurnWithTopUp); - let mut accounts = vec![ +impl Burn { + 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()); - // 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 - }, + 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..bab9f0f2ab 100644 --- a/sdk-libs/token-sdk/src/instruction/burn_checked.rs +++ b/sdk-libs/token-sdk/src/instruction/burn_checked.rs @@ -12,14 +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, -/// max_top_up: None, -/// fee_payer: None, +/// fee_payer, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` @@ -34,11 +34,8 @@ 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, + /// Fee payer for rent top-ups. + pub fee_payer: Pubkey, } /// # Burn ctoken via CPI with decimals validation: @@ -49,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, @@ -56,8 +54,7 @@ pub struct BurnChecked { /// decimals: 8, /// authority, /// system_program, -/// max_top_up: None, -/// fee_payer: None, +/// fee_payer, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -69,10 +66,8 @@ 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>, + /// Fee payer for rent top-ups. + pub fee_payer: AccountInfo<'info>, } impl<'info> BurnCheckedCpi<'info> { @@ -82,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) } } @@ -123,48 +108,34 @@ 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), + fee_payer: *cpi.fee_payer.key, } } } -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() { - AccountMeta::new(self.authority, true) - } else { - AccountMeta::new_readonly(self.authority, true) - }; +impl_with_top_up!(BurnChecked, BurnCheckedWithTopUp); - let mut accounts = vec![ +impl BurnChecked { + 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); - // 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 - }, + 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/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 ef6480017f..4762d0abc6 100644 --- a/sdk-libs/token-sdk/src/instruction/mint_to.rs +++ b/sdk-libs/token-sdk/src/instruction/mint_to.rs @@ -12,13 +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, -/// max_top_up: None, -/// fee_payer: None, +/// fee_payer, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` @@ -31,11 +31,8 @@ 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, + /// Fee payer for rent top-ups. + pub fee_payer: Pubkey, } /// # Mint to ctoken via CPI: @@ -46,14 +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, -/// max_top_up: None, -/// fee_payer: None, +/// fee_payer, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -64,10 +61,8 @@ 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>, + /// Fee payer for rent top-ups. + pub fee_payer: AccountInfo<'info>, } impl<'info> MintToCpi<'info> { @@ -77,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) } } @@ -127,47 +102,33 @@ 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), + fee_payer: *cpi.fee_payer.key, } } } -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() { - AccountMeta::new(self.authority, true) - } else { - AccountMeta::new_readonly(self.authority, true) - }; +impl_with_top_up!(MintTo, MintToWithTopUp); - let mut accounts = vec![ +impl MintTo { + 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()); - // 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 - }, + 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..e122801f7f 100644 --- a/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs +++ b/sdk-libs/token-sdk/src/instruction/mint_to_checked.rs @@ -12,14 +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, -/// max_top_up: None, -/// fee_payer: None, +/// fee_payer, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` @@ -34,11 +34,8 @@ 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, + /// Fee payer for rent top-ups. + pub fee_payer: Pubkey, } /// # Mint to ctoken via CPI with decimals validation: @@ -49,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, @@ -56,8 +54,7 @@ pub struct MintToChecked { /// decimals: 8, /// authority, /// system_program, -/// max_top_up: None, -/// fee_payer: None, +/// fee_payer, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -69,10 +66,8 @@ 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>, + /// Fee payer for rent top-ups. + pub fee_payer: AccountInfo<'info>, } impl<'info> MintToCheckedCpi<'info> { @@ -82,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) } } @@ -133,48 +108,34 @@ 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), + fee_payer: *cpi.fee_payer.key, } } } -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() { - AccountMeta::new(self.authority, true) - } else { - AccountMeta::new_readonly(self.authority, true) - }; +impl_with_top_up!(MintToChecked, MintToCheckedWithTopUp); - let mut accounts = vec![ +impl MintToChecked { + 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); - // 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 - }, + data, }) } } 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/revoke.rs b/sdk-libs/token-sdk/src/instruction/revoke.rs index d746dbe850..72bbfb8cf6 100644 --- a/sdk-libs/token-sdk/src/instruction/revoke.rs +++ b/sdk-libs/token-sdk/src/instruction/revoke.rs @@ -11,17 +11,21 @@ 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 (readonly signer) pub owner: Pubkey, + /// Fee payer for rent top-ups. + pub fee_payer: Pubkey, } /// # Revoke Light Token via CPI: @@ -31,10 +35,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 +49,8 @@ pub struct RevokeCpi<'info> { pub token_account: AccountInfo<'info>, pub owner: AccountInfo<'info>, pub system_program: AccountInfo<'info>, + /// Fee payer for rent top-ups. + pub fee_payer: AccountInfo<'info>, } impl<'info> RevokeCpi<'info> { @@ -52,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) } } @@ -68,6 +86,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, } } } @@ -78,8 +97,9 @@ 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), ], data: vec![5u8], // CTokenRevoke discriminator }) diff --git a/sdk-libs/token-sdk/src/instruction/transfer.rs b/sdk-libs/token-sdk/src/instruction/transfer.rs index 90985229a2..3c3ad5363f 100644 --- a/sdk-libs/token-sdk/src/instruction/transfer.rs +++ b/sdk-libs/token-sdk/src/instruction/transfer.rs @@ -12,13 +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, -/// max_top_up: None, -/// fee_payer: None, +/// fee_payer, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` @@ -27,12 +27,8 @@ 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, + /// Fee payer for rent top-ups. + pub fee_payer: Pubkey, } /// # Transfer ctoken via CPI: @@ -43,14 +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, -/// max_top_up: None, -/// fee_payer: None, +/// fee_payer, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -61,10 +57,8 @@ 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>, + /// Fee payer for rent top-ups. + pub fee_payer: AccountInfo<'info>, } impl<'info> TransferCpi<'info> { @@ -74,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) } } @@ -124,47 +98,33 @@ 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), + fee_payer: *account_infos.fee_payer.key, } } } -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() { - AccountMeta::new(self.authority, true) - } else { - AccountMeta::new_readonly(self.authority, true) - }; +impl_with_top_up!(Transfer, TransferWithTopUp); - let mut accounts = vec![ +impl Transfer { + 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()); - // 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 - }, + 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..d9d614c625 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,8 +21,7 @@ use solana_pubkey::Pubkey; /// amount: 100, /// decimals: 9, /// authority, -/// max_top_up: None, -/// fee_payer: None, +/// fee_payer, /// }.instruction()?; /// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` @@ -32,11 +32,8 @@ 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, + /// Fee payer for rent top-ups. + pub fee_payer: Pubkey, } /// # Transfer ctoken checked via CPI: @@ -48,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, @@ -56,8 +54,7 @@ pub struct TransferChecked { /// decimals: 9, /// authority, /// system_program, -/// max_top_up: None, -/// fee_payer: None, +/// fee_payer, /// } /// .invoke()?; /// # Ok::<(), solana_program_error::ProgramError>(()) @@ -70,10 +67,8 @@ 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>, + /// Fee payer for rent top-ups. + pub fee_payer: AccountInfo<'info>, } impl<'info> TransferCheckedCpi<'info> { @@ -83,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) } } @@ -139,50 +112,35 @@ 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), + fee_payer: *account_infos.fee_payer.key, } } } -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() { - AccountMeta::new(self.authority, true) - } else { - AccountMeta::new_readonly(self.authority, true) - }; +impl_with_top_up!(TransferChecked, TransferCheckedWithTopUp); - let mut accounts = vec![ +impl TransferChecked { + 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: { - // 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 - }, + 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..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,8 +103,8 @@ pub struct SplInterfaceCpi<'info> { /// decimals: 9, /// authority, /// payer, +/// mint, /// spl_interface: None, -/// max_top_up: None, /// source_owner: LIGHT_TOKEN_PROGRAM_ID, /// destination_owner: LIGHT_TOKEN_PROGRAM_ID, /// }.instruction()?; @@ -115,9 +117,8 @@ pub struct TransferInterface { pub decimals: u8, pub authority: Pubkey, pub payer: Pubkey, + pub mint: 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) @@ -128,13 +129,14 @@ 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, - max_top_up: self.max_top_up, - fee_payer: Some(self.payer), + fee_payer: self.payer, } .instruction(), @@ -211,8 +213,8 @@ 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), - max_top_up: None, source_owner: *cpi.source_account.owner, destination_owner: *cpi.destination_account.owner, } @@ -228,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 @@ -237,6 +240,7 @@ impl<'info> From<&TransferInterfaceCpi<'info>> for TransferInterface { /// authority, /// payer, /// compressed_token_program_authority, +/// mint, /// system_program, /// ) /// .invoke()?; @@ -250,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>, @@ -258,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( @@ -274,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 { @@ -284,6 +291,7 @@ impl<'info> TransferInterfaceCpi<'info> { decimals, payer, compressed_token_program_authority, + mint, spl_interface: None, system_program, } @@ -342,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, @@ -414,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/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. 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-libs/token-sdk/tests/instruction_transfer.rs b/sdk-libs/token-sdk/tests/instruction_transfer.rs index 7752bb14fe..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 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 (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, - max_top_up: None, - fee_payer: None, + fee_payer, } .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 + 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,57 +42,13 @@ fn test_transfer_basic() { ); } -/// 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 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_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 - 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 - ], - 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 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] -fn test_transfer_with_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 { @@ -102,70 +56,20 @@ fn test_transfer_with_fee_payer() { destination, amount: 100, authority, - max_top_up: None, - 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 (no max_top_up) 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 - ], - }; - - assert_eq!( - instruction, expected, - "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 + 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 @@ -176,6 +80,6 @@ fn test_transfer_with_max_top_up_and_fee_payer() { assert_eq!( instruction, expected, - "Transfer instruction with max_top_up and 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 d38238f773..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,8 +112,9 @@ fn test_transfer_interface_light_to_light_no_spl_interface() { decimals: 9, authority, payer, + mint, spl_interface: None, // No SPL interface needed - max_top_up: None, + source_owner: LIGHT_TOKEN_PROGRAM_ID, destination_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,8 +149,9 @@ fn test_transfer_interface_light_to_spl_requires_interface() { decimals: 9, authority, payer, + mint, spl_interface: None, // Missing required interface - max_top_up: None, + source_owner: LIGHT_TOKEN_PROGRAM_ID, destination_owner: SPL_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,8 +181,9 @@ fn test_transfer_interface_spl_to_light_requires_interface() { decimals: 9, authority, payer, + mint, spl_interface: None, // Missing required interface - max_top_up: None, + source_owner: SPL_TOKEN_PROGRAM_ID, destination_owner: LIGHT_TOKEN_PROGRAM_ID, }; @@ -207,13 +213,14 @@ 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, spl_interface_pda, spl_interface_pda_bump: 255, }), - max_top_up: None, + source_owner: LIGHT_TOKEN_PROGRAM_ID, destination_owner: 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,8 +250,9 @@ fn test_transfer_interface_spl_to_spl_requires_interface() { decimals: 9, authority, payer, + mint, spl_interface: None, // Missing interface - max_top_up: None, + source_owner: SPL_TOKEN_PROGRAM_ID, destination_owner: SPL_TOKEN_PROGRAM_ID, }; @@ -274,13 +283,14 @@ fn test_transfer_interface_spl_program_mismatch() { decimals: 9, authority, payer, + mint, spl_interface: Some(SplInterface { mint, spl_token_program: SPL_TOKEN_PROGRAM_ID, 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..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,8 +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(), - max_top_up: None, - 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 979530e4b6..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,8 +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(), - max_top_up: None, - 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 d9472349e0..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,8 +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(), - max_top_up: None, - 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 f9387ac017..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,8 +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(), - max_top_up: None, - fee_payer: None, + fee_payer: ctx.accounts.fee_payer.to_account_info(), } .invoke()?; } @@ -398,8 +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(), - max_top_up: None, - fee_payer: None, + fee_payer: ctx.accounts.fee_payer.to_account_info(), } .invoke()?; } @@ -1603,8 +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(), - max_top_up: None, - 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 5ccd85b84f..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,8 +341,7 @@ pub async fn setup_create_mint( destination: ata_pubkeys[*idx], amount: *amount, authority: mint_authority, - max_top_up: None, - 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..539adeac90 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()?; @@ -50,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); } @@ -74,8 +76,39 @@ pub fn process_approve_invoke_signed( owner: &accounts[2], system_program: &accounts[3], amount: data.amount, + 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 be576994ca..6a70debef0 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,9 +74,39 @@ pub fn process_burn_invoke_signed( amount, authority: &accounts[2], system_program: &accounts[4], - fee_payer: None, + fee_payer: &accounts[5], } .invoke_signed(&[signer])?; 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/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/ctoken_mint_to.rs b/sdk-tests/sdk-light-token-pinocchio/src/ctoken_mint_to.rs index 95920489aa..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 @@ -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,9 +75,39 @@ 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])?; 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..d653123ac8 100644 --- a/sdk-tests/sdk-light-token-pinocchio/src/lib.rs +++ b/sdk-tests/sdk-light-token-pinocchio/src/lib.rs @@ -19,27 +19,44 @@ 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_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::{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 +151,24 @@ 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, + /// 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 { @@ -174,6 +209,15 @@ 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), + 41 => Ok(InstructionType::CreateTokenAccountInvokeWith), + 42 => Ok(InstructionType::CreateTokenAccountInvokeSignedWith), + 43 => Ok(InstructionType::CreateAtaInvokeWith), + 44 => Ok(InstructionType::CreateAtaIdempotentInvokeWith), _ => Err(ProgramError::InvalidInstructionData), } } @@ -321,6 +365,47 @@ 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), + 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/src/revoke.rs b/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs index a1fb1508f9..aa0ec2bc0f 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()?; @@ -36,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); } @@ -57,8 +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[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 4958e1faa3..5458cc5c8b 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 @@ -93,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/src/transfer_checked.rs b/sdk-tests/sdk-light-token-pinocchio/src/transfer_checked.rs index 7bf130bce1..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: None, + fee_payer: &accounts[5], } .invoke()?; @@ -54,11 +55,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 +80,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/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/shared/mod.rs b/sdk-tests/sdk-light-token-pinocchio/tests/shared/mod.rs index 7e35bdd140..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,8 +127,7 @@ pub async fn setup_create_mint( destination: ata_pubkeys[*idx], amount: *amount, authority: mint_authority, - max_top_up: None, - fee_payer: None, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); @@ -248,8 +247,7 @@ 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, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); @@ -388,8 +386,7 @@ 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, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); 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..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,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(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 ], 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(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 ], 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 ece1d29f34..21f4614175 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, }; @@ -149,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_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); } 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..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 @@ -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: @@ -234,9 +314,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, 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..e2e1852721 100644 --- a/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs @@ -115,6 +115,7 @@ 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(payer.pubkey(), true), // fee_payer (PDA authority != tx fee payer) AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), ], data: instruction_data, @@ -134,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); +} 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..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}, @@ -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, @@ -347,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-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/approve.rs b/sdk-tests/sdk-light-token-test/src/approve.rs index 0dda096b88..4987081344 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()?; @@ -46,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); } @@ -69,8 +71,39 @@ pub fn process_approve_invoke_signed( owner: accounts[2].clone(), system_program: accounts[3].clone(), amount: data.amount, + 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 d1b2bfbf2f..0daa2d0ab9 100644 --- a/sdk-tests/sdk-light-token-test/src/burn.rs +++ b/sdk-tests/sdk-light-token-test/src/burn.rs @@ -29,8 +29,7 @@ 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, + fee_payer: accounts[2].clone(), } .invoke()?; @@ -45,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); } @@ -68,10 +68,39 @@ pub fn process_burn_invoke_signed( amount, authority: accounts[2].clone(), system_program: accounts[4].clone(), - max_top_up: None, - fee_payer: None, + fee_payer: accounts[5].clone(), } .invoke_signed(&[signer_seeds])?; 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 9b4d10c61b..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 @@ -29,8 +29,7 @@ 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, + fee_payer: accounts[2].clone(), } .invoke()?; @@ -45,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); } @@ -68,10 +68,39 @@ pub fn process_mint_to_invoke_signed( amount, authority: accounts[2].clone(), system_program: accounts[3].clone(), - max_top_up: None, - fee_payer: None, + fee_payer: accounts[5].clone(), } .invoke_signed(&[signer_seeds])?; 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..b3404f4e41 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,27 @@ 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 +420,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 +567,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 ff55fbecce..9ce58c0adc 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()?; @@ -32,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); } @@ -50,8 +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[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 2ddcf397e5..5597996e40 100644 --- a/sdk-tests/sdk-light-token-test/src/transfer.rs +++ b/sdk-tests/sdk-light-token-test/src/transfer.rs @@ -36,8 +36,7 @@ pub fn process_transfer_invoke( amount: data.amount, authority: accounts[2].clone(), system_program: accounts[3].clone(), - max_top_up: None, - fee_payer: None, + fee_payer: accounts[2].clone(), } .invoke()?; @@ -56,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); } @@ -79,8 +79,7 @@ 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, + fee_payer: accounts[4].clone(), }; // Invoke with PDA signing - the builder handles instruction creation and invoke_signed CPI @@ -89,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/src/transfer_checked.rs b/sdk-tests/sdk-light-token-test/src/transfer_checked.rs index d31f1b2e4f..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,8 +36,7 @@ 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, + fee_payer: accounts[5].clone(), } .invoke()?; @@ -51,11 +51,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); } @@ -75,8 +76,7 @@ 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, + fee_payer: accounts[5].clone(), }; // Invoke with PDA signing 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/scenario_light_mint.rs b/sdk-tests/sdk-light-token-test/tests/scenario_light_mint.rs index 30851e769d..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,8 +93,7 @@ async fn test_mint_to_ctoken_scenario() { destination: ctoken_ata2, amount: transfer_amount, authority: owner1.pubkey(), - max_top_up: None, - 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 5651cf95a2..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,8 +98,7 @@ 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, + 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 79f591ffa3..d425c3d229 100644 --- a/sdk-tests/sdk-light-token-test/tests/shared.rs +++ b/sdk-tests/sdk-light-token-test/tests/shared.rs @@ -123,8 +123,7 @@ pub async fn setup_create_mint( destination: ata_pubkeys[*idx], amount: *amount, authority: mint_authority, - max_top_up: None, - fee_payer: None, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); @@ -244,8 +243,7 @@ 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, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); @@ -384,8 +382,7 @@ 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, + fee_payer: payer.pubkey(), } .instruction() .unwrap(); 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 1eb3a70b2d..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, }; @@ -122,6 +123,7 @@ async fn test_burn_invoke_signed() { 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(payer.pubkey(), true), // fee_payer ], data: instruction_data, }; @@ -143,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 69b6e822e7..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 @@ -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, }; @@ -251,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 011494801c..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, }; @@ -109,6 +110,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_readonly(LIGHT_TOKEN_PROGRAM_ID, false), ], data: instruction_data, @@ -128,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); +} 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..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}, @@ -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, @@ -338,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); +} 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 {