Skip to content

Commit b5b5282

Browse files
committed
feat: sdk decompress full transfer2
1 parent 5d8b609 commit b5b5282

File tree

19 files changed

+916
-52
lines changed

19 files changed

+916
-52
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

program-libs/ctoken-types/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ profile-heap = ["dep:light-heap"]
1414
borsh = { workspace = true }
1515
# Solana dependencies
1616
solana-pubkey = { workspace = true }
17+
solana-account-info = { workspace = true }
1718
solana-program-error = { workspace = true, optional = true }
1819
light-zero-copy = { workspace = true, features = ["derive", "mut"] }
1920
light-compressed-account = { workspace = true }

program-libs/ctoken-types/src/instructions/transfer2.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@ use crate::{AnchorDeserialize, AnchorSerialize, CTokenError};
1111

1212
#[repr(C)]
1313
#[derive(
14-
Debug, Clone, Default, PartialEq, AnchorSerialize, AnchorDeserialize, ZeroCopy, ZeroCopyMut,
14+
Debug,
15+
Copy,
16+
Clone,
17+
Default,
18+
PartialEq,
19+
AnchorSerialize,
20+
AnchorDeserialize,
21+
ZeroCopy,
22+
ZeroCopyMut,
1523
)]
1624
pub struct MultiInputTokenDataWithContext {
1725
pub owner: u8,

programs/compressed-token/program/src/shared/cpi.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::mem::MaybeUninit;
22

33
use anchor_lang::solana_program::program_error::ProgramError;
4+
use light_profiler::profile;
45
use light_sdk_types::{
56
ACCOUNT_COMPRESSION_AUTHORITY_PDA, ACCOUNT_COMPRESSION_PROGRAM_ID, CPI_AUTHORITY_PDA_SEED,
67
LIGHT_SYSTEM_PROGRAM_ID, REGISTERED_PROGRAM_PDA,
@@ -29,6 +30,7 @@ use crate::LIGHT_CPI_SIGNER;
2930
///
3031
/// # Returns
3132
/// * `Result<(), ProgramError>` - Success or error from the CPI call
33+
#[profile]
3234
pub fn execute_cpi_invoke(
3335
accounts: &[AccountInfo],
3436
cpi_bytes: Vec<u8>,

programs/compressed-token/program/src/shared/cpi_bytes_size.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ const MAX_OUTPUT_ACCOUNTS: usize = 35;
1818

1919
/// Calculate data length for a compressed mint account
2020
#[profile]
21+
#[inline(always)]
2122
pub fn mint_data_len(config: &light_ctoken_types::state::CompressedMintConfig) -> u32 {
2223
use light_ctoken_types::state::CompressedMint;
2324
CompressedMint::byte_len(config).unwrap() as u32
2425
}
2526

2627
/// Calculate data length for a compressed token account
2728
#[profile]
29+
#[inline(always)]
2830
pub fn token_data_len(has_delegate: bool) -> u32 {
2931
if has_delegate {
3032
107
@@ -90,6 +92,7 @@ impl CpiConfigInput {
9092
// TODO: generalize and move the light-compressed-account
9193
// TODO: add version of this function with hardcoded values that just calculates the cpi_byte_size, with a randomized test vs this function
9294
#[profile]
95+
#[inline(always)]
9396
pub fn cpi_bytes_config(input: CpiConfigInput) -> InstructionDataInvokeCpiWithReadOnlyConfig {
9497
let input_compressed_accounts = {
9598
let mut input_compressed_accounts = Vec::with_capacity(input.input_accounts.len());
@@ -120,10 +123,11 @@ pub fn cpi_bytes_config(input: CpiConfigInput) -> InstructionDataInvokeCpiWithRe
120123

121124
outputs
122125
};
126+
let new_address_params = vec![(); input.new_address_params];
123127
InstructionDataInvokeCpiWithReadOnlyConfig {
124128
cpi_context: (),
125129
proof: (input.has_proof, ()),
126-
new_address_params: (0..input.new_address_params).map(|_| ()).collect(), // Create required number of new address params
130+
new_address_params, // Create required number of new address params
127131
input_compressed_accounts,
128132
output_compressed_accounts,
129133
read_only_addresses: vec![],
@@ -133,6 +137,7 @@ pub fn cpi_bytes_config(input: CpiConfigInput) -> InstructionDataInvokeCpiWithRe
133137

134138
/// Allocate CPI instruction bytes with discriminator and length prefix
135139
#[profile]
140+
#[inline(always)]
136141
pub fn allocate_invoke_with_read_only_cpi_bytes(
137142
config: &InstructionDataInvokeCpiWithReadOnlyConfig,
138143
) -> Vec<u8> {

sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,3 @@
1-
use crate::{
2-
account2::CTokenAccount2,
3-
error::TokenSdkError,
4-
instructions::{
5-
transfer2::{
6-
account_metas::Transfer2AccountsMetaConfig, create_transfer2_instruction,
7-
Transfer2Config, Transfer2Inputs,
8-
},
9-
CTokenDefaultAccounts,
10-
},
11-
};
121
use light_compressed_account::instruction_data::cpi_context::CompressedCpiContext;
132
use light_ctoken_types::state::{CompressedToken, ZExtensionStruct};
143
use light_profiler::profile;
@@ -22,6 +11,18 @@ use solana_instruction::{AccountMeta, Instruction};
2211
use solana_msg::msg;
2312
use solana_pubkey::Pubkey;
2413

14+
use crate::{
15+
account2::CTokenAccount2,
16+
error::TokenSdkError,
17+
instructions::{
18+
transfer2::{
19+
account_metas::Transfer2AccountsMetaConfig, create_transfer2_instruction,
20+
Transfer2Config, Transfer2Inputs,
21+
},
22+
CTokenDefaultAccounts,
23+
},
24+
};
25+
2526
/// Struct to hold all the indices needed for CompressAndClose operation
2627
#[derive(Debug, crate::AnchorSerialize, crate::AnchorDeserialize)]
2728
pub struct CompressAndCloseIndices {
@@ -46,14 +47,22 @@ pub fn pack_for_compress_and_close(
4647
let output_tree_index = packed_accounts.insert_or_get(output_queue);
4748
let (ctoken_account, _) = CompressedToken::zero_copy_at(ctoken_account_data)?;
4849
let source_index = packed_accounts.insert_or_get(ctoken_account_pubkey);
49-
let mint_index = packed_accounts.insert_or_get((*ctoken_account.mint).into());
50-
let owner_index = packed_accounts.insert_or_get((*ctoken_account.owner).into());
50+
let mint_index = packed_accounts.insert_or_get(Pubkey::from(ctoken_account.mint.to_bytes()));
51+
let owner_index = packed_accounts.insert_or_get(Pubkey::from(ctoken_account.owner.to_bytes()));
5152
let authority_index = if signer_is_rent_authority {
5253
// Rent authority is separate and will be added as a signer later
53-
packed_accounts.insert_or_get_config((*ctoken_account.owner).into(), false, false)
54+
packed_accounts.insert_or_get_config(
55+
Pubkey::from(ctoken_account.owner.to_bytes()),
56+
false,
57+
false,
58+
)
5459
} else {
5560
// Owner is the authority and needs to sign
56-
packed_accounts.insert_or_get_config((*ctoken_account.owner).into(), true, false)
61+
packed_accounts.insert_or_get_config(
62+
Pubkey::from(ctoken_account.owner.to_bytes()),
63+
true,
64+
false,
65+
)
5766
};
5867

5968
let rent_recipient_index = if signer_is_rent_authority {
@@ -62,8 +71,13 @@ pub fn pack_for_compress_and_close(
6271
if let Some(extensions) = &ctoken_account.extensions {
6372
for extension in extensions {
6473
if let ZExtensionStruct::Compressible(e) = extension {
65-
packed_accounts.insert_or_get_config((e.rent_authority).into(), true, true);
66-
recipient_index = packed_accounts.insert_or_get((e.rent_recipient).into());
74+
packed_accounts.insert_or_get_config(
75+
Pubkey::from(e.rent_authority.to_bytes()),
76+
true,
77+
true,
78+
);
79+
recipient_index =
80+
packed_accounts.insert_or_get(Pubkey::from(e.rent_recipient.to_bytes()));
6781
break;
6882
}
6983
}
@@ -327,7 +341,7 @@ pub fn compress_and_close_ctoken_accounts<'info>(
327341

328342
// Find indices for all required accounts
329343
let indices = find_account_indices(
330-
&find_index,
344+
find_index,
331345
ctoken_account_info.key,
332346
&mint_pubkey,
333347
&owner_pubkey,
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
use light_compressed_account::{
2+
compressed_account::PackedMerkleContext, instruction_data::cpi_context::CompressedCpiContext,
3+
};
4+
use light_ctoken_types::instructions::transfer2::MultiInputTokenDataWithContext;
5+
use light_profiler::profile;
6+
use light_sdk::{
7+
error::LightSdkError,
8+
instruction::{AccountMetasVec, PackedAccounts, PackedStateTreeInfo, SystemAccountMetaConfig},
9+
token::TokenData,
10+
};
11+
use solana_account_info::AccountInfo;
12+
use solana_instruction::{AccountMeta, Instruction};
13+
use solana_pubkey::Pubkey;
14+
15+
use crate::{
16+
account2::CTokenAccount2,
17+
error::TokenSdkError,
18+
instructions::{
19+
transfer2::{
20+
account_metas::Transfer2AccountsMetaConfig, create_transfer2_instruction,
21+
Transfer2Config, Transfer2Inputs,
22+
},
23+
CTokenDefaultAccounts,
24+
},
25+
ValidityProof,
26+
};
27+
28+
/// Struct to hold all the data needed for DecompressFull operation
29+
/// Contains the complete compressed account data and destination index
30+
#[derive(Debug, Clone, crate::AnchorSerialize, crate::AnchorDeserialize)]
31+
pub struct DecompressFullIndices {
32+
pub source: MultiInputTokenDataWithContext, // Complete compressed account data with merkle context
33+
pub destination_index: u8, // Destination ctoken Solana account (must exist)
34+
}
35+
36+
/// Decompress full balance from compressed token accounts with pre-computed indices
37+
///
38+
/// # Arguments
39+
/// * `fee_payer` - The fee payer pubkey
40+
/// * `validity_proof` - Validity proof for the compressed accounts (zkp or index)
41+
/// * `cpi_context_pubkey` - Optional CPI context account for optimized multi-program transactions
42+
/// * `indices` - Slice of source/destination pairs for decompress operations
43+
/// * `packed_accounts` - Slice of all accounts that will be used in the instruction
44+
///
45+
/// # Returns
46+
/// An instruction that decompresses the full balance of all provided token accounts
47+
#[profile]
48+
pub fn decompress_full_ctoken_accounts_with_indices<'info>(
49+
fee_payer: Pubkey,
50+
validity_proof: ValidityProof,
51+
cpi_context_pubkey: Option<Pubkey>,
52+
indices: &[DecompressFullIndices],
53+
packed_accounts: &[AccountInfo<'info>],
54+
) -> Result<Instruction, TokenSdkError> {
55+
if indices.is_empty() {
56+
return Err(TokenSdkError::InvalidAccountData);
57+
}
58+
59+
// Process each set of indices
60+
let mut token_accounts = Vec::with_capacity(indices.len());
61+
62+
for idx in indices.iter() {
63+
// Create CTokenAccount2 with the source data
64+
// For decompress_full, we don't have an output tree since everything goes to the destination
65+
let mut token_account = CTokenAccount2::new(
66+
vec![idx.source],
67+
0, // No output tree for full decompress
68+
)?;
69+
70+
// Set up decompress_full - decompress entire balance to destination ctoken account
71+
token_account.decompress(idx.source.amount, idx.destination_index)?;
72+
token_accounts.push(token_account);
73+
}
74+
75+
// Convert packed_accounts to AccountMetas
76+
let mut packed_account_metas = Vec::with_capacity(packed_accounts.len());
77+
for info in packed_accounts.iter() {
78+
packed_account_metas.push(AccountMeta {
79+
pubkey: *info.key,
80+
is_signer: info.is_signer,
81+
is_writable: info.is_writable,
82+
});
83+
}
84+
85+
let (meta_config, transfer_config) = if let Some(cpi_context) = cpi_context_pubkey {
86+
let cpi_context_config = CompressedCpiContext {
87+
set_context: false,
88+
first_set_context: false,
89+
cpi_context_account_index: 0,
90+
};
91+
92+
(
93+
Transfer2AccountsMetaConfig {
94+
fee_payer: Some(fee_payer),
95+
cpi_context: Some(cpi_context),
96+
decompressed_accounts_only: false,
97+
sol_pool_pda: None,
98+
sol_decompression_recipient: None,
99+
with_sol_pool: false,
100+
packed_accounts: Some(packed_account_metas),
101+
},
102+
Transfer2Config::default()
103+
.filter_zero_amount_outputs()
104+
.with_cpi_context(cpi_context, cpi_context_config),
105+
)
106+
} else {
107+
(
108+
Transfer2AccountsMetaConfig::new(fee_payer, packed_account_metas),
109+
Transfer2Config::default().filter_zero_amount_outputs(),
110+
)
111+
};
112+
113+
// Create the transfer2 instruction with all decompress operations
114+
let inputs = Transfer2Inputs {
115+
meta_config,
116+
token_accounts,
117+
transfer_config,
118+
validity_proof,
119+
..Default::default()
120+
};
121+
122+
create_transfer2_instruction(inputs)
123+
}
124+
125+
/// Helper function to pack compressed token accounts into DecompressFullIndices
126+
/// Used in tests to build indices for multiple compressed accounts to decompress
127+
///
128+
/// # Arguments
129+
/// * `token_data` - Slice of TokenData from compressed accounts
130+
/// * `tree_infos` - Packed tree info for each compressed account
131+
/// * `destination_indices` - Destination account indices for each decompression
132+
/// * `packed_accounts` - PackedAccounts that will be used to insert/get indices
133+
///
134+
/// # Returns
135+
/// Vec of DecompressFullIndices ready to use with decompress_full_ctoken_accounts_with_indices
136+
#[profile]
137+
pub fn pack_for_decompress_full(
138+
token: &TokenData,
139+
tree_info: &PackedStateTreeInfo,
140+
destination: Pubkey,
141+
packed_accounts: &mut PackedAccounts,
142+
) -> DecompressFullIndices {
143+
let source = MultiInputTokenDataWithContext {
144+
owner: packed_accounts.insert_or_get_config(token.owner, true, false),
145+
amount: token.amount,
146+
has_delegate: token.delegate.is_some(),
147+
delegate: token
148+
.delegate
149+
.map(|d| packed_accounts.insert_or_get(d))
150+
.unwrap_or(0),
151+
mint: packed_accounts.insert_or_get(token.mint),
152+
version: 2,
153+
merkle_context: PackedMerkleContext {
154+
merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
155+
queue_pubkey_index: tree_info.queue_pubkey_index,
156+
prove_by_index: tree_info.prove_by_index,
157+
leaf_index: tree_info.leaf_index,
158+
},
159+
root_index: tree_info.root_index,
160+
};
161+
162+
DecompressFullIndices {
163+
source,
164+
destination_index: packed_accounts.insert_or_get(destination),
165+
}
166+
}
167+
168+
pub struct DecompressFullAccounts {
169+
pub compressed_token_program: Pubkey,
170+
pub cpi_authority_pda: Pubkey,
171+
pub cpi_context: Option<Pubkey>,
172+
pub self_program: Pubkey,
173+
}
174+
175+
impl DecompressFullAccounts {
176+
pub fn new(self_program: Pubkey, cpi_context: Option<Pubkey>) -> Self {
177+
Self {
178+
compressed_token_program: CTokenDefaultAccounts::default().compressed_token_program,
179+
cpi_authority_pda: CTokenDefaultAccounts::default().cpi_authority_pda,
180+
cpi_context,
181+
self_program,
182+
}
183+
}
184+
}
185+
186+
impl AccountMetasVec for DecompressFullAccounts {
187+
/// Adds:
188+
/// 1. system accounts if not set
189+
/// 2. compressed token program and ctoken cpi authority pda to pre accounts
190+
fn get_account_metas_vec(&self, accounts: &mut PackedAccounts) -> Result<(), LightSdkError> {
191+
if !accounts.system_accounts_set() {
192+
let config = SystemAccountMetaConfig {
193+
self_program: self.self_program,
194+
cpi_context: self.cpi_context,
195+
..Default::default()
196+
};
197+
accounts.add_system_accounts_small(config)?;
198+
}
199+
// Add both accounts in one operation for better performance
200+
accounts.pre_accounts.extend_from_slice(&[
201+
AccountMeta {
202+
pubkey: self.compressed_token_program,
203+
is_signer: false,
204+
is_writable: false,
205+
},
206+
AccountMeta {
207+
pubkey: self.cpi_authority_pda,
208+
is_signer: false,
209+
is_writable: false,
210+
},
211+
]);
212+
Ok(())
213+
}
214+
}

0 commit comments

Comments
 (0)