Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,57 @@ Format and clippy checks across the entire codebase.
- **`program-tests/`**: Integration tests requiring Solana runtime, depend on `light-test-utils`
- **`sdk-tests/`**: SDK-specific integration tests
- **Special case**: `zero-copy-derive-test` in `program-tests/` only to break cyclic dependencies

### Test Assertion Pattern

When testing account state, use borsh deserialization with a single `assert_eq` against an expected reference account:

```rust
use borsh::BorshDeserialize;
use light_ctoken_types::state::{
AccountState, CToken, ExtensionStruct, PausableAccountExtension,
PermanentDelegateAccountExtension,
};

// Deserialize the account
let ctoken = CToken::deserialize(&mut &account.data[..])
.expect("Failed to deserialize CToken account");

// Extract runtime-specific values from deserialized account
let compression_info = ctoken
.extensions
.as_ref()
.and_then(|exts| {
exts.iter().find_map(|e| match e {
ExtensionStruct::Compressible(info) => Some(info.clone()),
_ => None,
})
})
.expect("Should have Compressible extension");

// Build expected account for comparison
let expected_ctoken = CToken {
mint: mint_pubkey.to_bytes().into(),
owner: payer.pubkey().to_bytes().into(),
amount: 0,
delegate: None,
state: AccountState::Frozen,
is_native: None,
delegated_amount: 0,
close_authority: None,
extensions: Some(vec![
ExtensionStruct::Compressible(compression_info),
ExtensionStruct::PausableAccount(PausableAccountExtension),
ExtensionStruct::PermanentDelegateAccount(PermanentDelegateAccountExtension),
]),
};

// Single assert comparing full account state
assert_eq!(ctoken, expected_ctoken, "CToken account should match expected");
```
Comment thread
ananas-block marked this conversation as resolved.

**Benefits:**
- Type-safe assertions using actual struct fields instead of magic byte offsets
- Maintainable - if account layout changes, deserialization handles it
- Readable - clear field names vs `account.data[108]`
- Single assertion point for the entire account state
118 changes: 25 additions & 93 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ groth16-solana = { version = "0.2.0" }
bytemuck = { version = "1.19.0" }
arrayvec = "0.7"
tinyvec = "1.10.0"
pinocchio-token-program = { git= "https://github.com/Lightprotocol/token", rev="38d8634353e5eeb8c015d364df0eaa39f5c48b05" }
pinocchio-token-program = { git= "https://github.com/Lightprotocol/token", rev="5d2768b98075ba03dfc5d6e6dd8567ba065c84ba" }
# Math and crypto
num-bigint = "0.4.6"
tabled = "0.20"
Expand Down
15 changes: 13 additions & 2 deletions forester/src/compressible/compressor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ impl<R: Rpc> Compressor<R> {
.ok_or_else(|| anyhow::anyhow!("Account missing compressible extension"))?;

// Determine owner based on compress_to_pubkey flag
let compressed_token_owner = if compressible_ext.compress_to_pubkey != 0 {
let compressed_token_owner = if compressible_ext.info.compress_to_pubkey != 0 {
account_state.pubkey // Use account pubkey for PDAs
} else {
Pubkey::new_from_array(account_state.account.owner.to_bytes()) // Use original owner
Expand All @@ -136,14 +136,25 @@ impl<R: Rpc> Compressor<R> {
let owner_index = packed_accounts.insert_or_get(compressed_token_owner);

// Extract rent_sponsor from extension
let rent_sponsor = Pubkey::new_from_array(compressible_ext.rent_sponsor);
let rent_sponsor = Pubkey::new_from_array(compressible_ext.info.rent_sponsor);
let rent_sponsor_index = packed_accounts.insert_or_get(rent_sponsor);

// Handle delegate if present
let delegate_index = account_state
.account
.delegate
.map(|delegate| {
let delegate_pubkey = Pubkey::new_from_array(delegate.to_bytes());
packed_accounts.insert_or_get(delegate_pubkey)
})
.unwrap_or(0);

indices_vec.push(CompressAndCloseIndices {
source_index,
mint_index,
owner_index,
rent_sponsor_index,
delegate_index,
});
Comment on lines +142 to 158
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n --type=rust 'struct CompressAndCloseIndices' -A 10

Repository: Lightprotocol/light-protocol

Length of output: 200


🏁 Script executed:

rg -n --type=rust 'delegate_index' -B 3 -A 3

Repository: Lightprotocol/light-protocol

Length of output: 27055


🏁 Script executed:

rg -n --type=rust 'CompressAndCloseIndices' --type=rust -A 5 | grep -E '(delegate_index|fn |struct )' | head -40

Repository: Lightprotocol/light-protocol

Length of output: 642


🏁 Script executed:

sed -n '100,170p' forester/src/compressible/compressor.rs

Repository: Lightprotocol/light-protocol

Length of output: 3016


🏁 Script executed:

rg -n 'struct CompressAndCloseIndices' -A 10 forester/src/compressible/compressor.rs

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

rg -n 'has_delegate' forester/src/compressible/compressor.rs -B 2 -A 2

Repository: Lightprotocol/light-protocol

Length of output: 54


Add disambiguation for delegate_index = 0 sentinel — output queue also at index 0.

The delegate index defaults to 0 when absent, but the output queue is also inserted at index 0 (line 101). This creates an ambiguity: downstream code cannot distinguish "no delegate (index 0)" from "delegate at index 0 (output queue)."

Similar code patterns (e.g., MultiTokenTransferOutputData in programs/compressed-token) use either Option<u8> or a separate has_delegate flag. Consider one of these approaches:

  • Use Option<u8> instead of u8 with 0 as sentinel
  • Add a has_delegate: bool field to CompressAndCloseIndices
  • Document explicitly that index 0 cannot be a valid delegate and why
🤖 Prompt for AI Agents
In forester/src/compressible/compressor.rs around lines 142 to 158, the code
uses 0 as a sentinel for "no delegate" but index 0 is also used by the output
queue, causing ambiguity; change CompressAndCloseIndices.delegate_index to
represent presence explicitly (either make it Option<u8> or add a has_delegate:
bool next to delegate_index), update the construction here to set delegate_index
= Some(packed_accounts.insert_or_get(...)) or set has_delegate=true when a
delegate exists (and None/false when absent), and propagate this change to all
downstream uses of CompressAndCloseIndices so they check the Option or the
has_delegate flag instead of assuming 0 means absent.

}

Expand Down
Loading
Loading