use borsh::BorshDeserialize;
use light_sdk_types::lca::{
compressed_account::{
CompressedAccount, CompressedAccountData, PackedCompressedAccountWithMerkleContext,
},
constants::{
ACCOUNT_COMPRESSION_PROGRAM_ID, CREATE_CPI_CONTEXT_ACCOUNT, LIGHT_REGISTRY_PROGRAM_ID,
LIGHT_SYSTEM_PROGRAM_ID, REGISTERED_PROGRAM_PDA,
},
discriminators::*,
instruction_data::{
data::{InstructionDataInvoke, OutputCompressedAccountWithPackedContext},
insert_into_queues::InsertIntoQueuesInstructionData,
with_account_info::InstructionDataInvokeCpiWithAccountInfo,
with_readonly::InstructionDataInvokeCpiWithReadOnly,
},
nullifier::create_nullifier,
Pubkey,
};
use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID;
use light_zero_copy::traits::ZeroCopyAt;
const TRANSFER2: u8 = 101;
use super::{
error::ParseIndexerEventError,
event::{
BatchNullifyContext, BatchPublicTransactionEvent, MerkleTreeSequenceNumber,
MerkleTreeSequenceNumberV1, NewAddress, PublicTransactionEvent,
},
};
#[derive(Debug, Clone, PartialEq)]
struct ExecutingSystemInstruction<'a> {
output_compressed_accounts: Vec<OutputCompressedAccountWithPackedContext>,
input_compressed_accounts: Vec<PackedCompressedAccountWithMerkleContext>,
is_compress: bool,
relay_fee: Option<u64>,
compress_or_decompress_lamports: Option<u64>,
execute_cpi_context: bool,
accounts: &'a [Pubkey],
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Indices {
pub system: usize,
pub cpi: Vec<usize>,
pub insert_into_queues: usize,
pub found_solana_system_program_instruction: bool,
pub found_system: bool,
pub token: Option<usize>,
pub found_registry: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ProgramId {
LightSystem,
AccountCompression,
SolanaSystem,
LightToken,
Registry,
Unknown,
}
#[derive(Debug, Clone, PartialEq)]
struct AssociatedInstructions<'a> {
pub executing_system_instruction: ExecutingSystemInstruction<'a>,
pub cpi_context_outputs: Vec<OutputCompressedAccountWithPackedContext>,
pub insert_into_queues_instruction: InsertIntoQueuesInstructionData<'a>,
pub accounts: &'a [Pubkey],
}
pub fn event_from_light_transaction(
program_ids: &[Pubkey],
instructions: &[Vec<u8>],
accounts: Vec<Vec<Pubkey>>,
) -> Result<Option<Vec<BatchPublicTransactionEvent>>, ParseIndexerEventError> {
let program_ids = wrap_program_ids(program_ids, instructions, &accounts);
let mut patterns = find_cpi_patterns(&program_ids);
if patterns.is_empty() {
return Ok(None);
}
patterns.reverse();
let associated_instructions = patterns
.iter()
.map(|pattern| deserialize_associated_instructions(pattern, instructions, &accounts))
.collect::<Result<Vec<_>, _>>()?;
let batched_transaction_events = associated_instructions
.iter()
.map(|associated_instruction| create_batched_transaction_event(associated_instruction))
.collect::<Result<Vec<_>, _>>()?;
Ok(Some(batched_transaction_events))
}
fn deserialize_associated_instructions<'a>(
indices: &Indices,
instructions: &'a [Vec<u8>],
accounts: &'a [Vec<Pubkey>],
) -> Result<AssociatedInstructions<'a>, ParseIndexerEventError> {
let (insert_queues_instruction, cpi_context_outputs) = {
let ix = &instructions[indices.insert_into_queues];
if ix.len() < 12 {
return Err(ParseIndexerEventError::InstructionDataTooSmall(
ix.len(),
12,
));
}
let discriminator: [u8; 8] = ix[0..8].try_into().unwrap();
if discriminator == DISCRIMINATOR_INSERT_INTO_QUEUES {
let (data, bytes) = InsertIntoQueuesInstructionData::zero_copy_at(&ix[12..])?;
let cpi_context_outputs =
Vec::<OutputCompressedAccountWithPackedContext>::deserialize(&mut &bytes[..])?;
Ok((data, cpi_context_outputs))
} else {
Err(ParseIndexerEventError::DeserializeAccountLightSystemCpiInputsError)
}
}?;
let exec_instruction =
deserialize_instruction(&instructions[indices.system], &accounts[indices.system])?;
Ok(AssociatedInstructions {
executing_system_instruction: exec_instruction,
cpi_context_outputs,
insert_into_queues_instruction: insert_queues_instruction,
accounts: &accounts[indices.insert_into_queues][2..],
})
}
pub fn find_cpi_patterns(program_ids: &[ProgramId]) -> Vec<Indices> {
let mut vec = Vec::new();
let mut next_index = usize::MAX;
for (last_index, program_id) in (0..program_ids.len()).rev().zip(program_ids.iter().rev()) {
if last_index > next_index {
continue;
}
if let ProgramId::AccountCompression = program_id {
let (res, last_index) = find_cpi_pattern(last_index, program_ids);
next_index = last_index;
if let Some(res) = res {
vec.push(res);
};
}
}
vec
}
pub fn find_cpi_pattern(start_index: usize, program_ids: &[ProgramId]) -> (Option<Indices>, usize) {
let mut index_account = Indices {
insert_into_queues: start_index,
..Default::default()
};
let mut tentative_token: Option<usize> = None;
for (index, program_id) in (0..start_index)
.rev()
.zip(program_ids[..start_index].iter().rev())
{
if let ProgramId::SolanaSystem = program_id {
index_account.found_solana_system_program_instruction = true;
continue;
} else if matches!(program_id, ProgramId::LightSystem)
&& index_account.found_solana_system_program_instruction
&& !index_account.found_system
{
index_account.system = index;
index_account.found_system = true;
} else if index_account.found_system && matches!(program_id, ProgramId::LightSystem) {
index_account.cpi.push(index);
} else if index_account.found_system && matches!(program_id, ProgramId::LightToken) {
if tentative_token.is_none() {
tentative_token = Some(index);
}
} else if index_account.found_system && matches!(program_id, ProgramId::Registry) {
index_account.found_registry = true;
if index_account.token.is_none() {
index_account.token = tentative_token;
}
} else if matches!(program_id, ProgramId::AccountCompression) && index_account.found_system
{
return (Some(index_account), index);
} else if !index_account.found_system {
return (None, index);
}
}
if index_account.found_system {
(Some(index_account), 0)
} else {
(None, 0)
}
}
pub fn wrap_program_ids(
program_ids: &[Pubkey],
instructions: &[Vec<u8>],
accounts: &[Vec<Pubkey>],
) -> Vec<ProgramId> {
let mut vec = Vec::new();
for ((instruction, program_id), accounts) in instructions
.iter()
.zip(program_ids.iter())
.zip(accounts.iter())
{
if instruction.len() < 12 {
vec.push(ProgramId::Unknown);
continue;
}
let discriminator: [u8; 8] = instruction[0..8].try_into().unwrap();
if program_id == &Pubkey::default() {
vec.push(ProgramId::SolanaSystem);
} else if program_id == &Pubkey::from(LIGHT_SYSTEM_PROGRAM_ID) {
if discriminator == CREATE_CPI_CONTEXT_ACCOUNT {
vec.push(ProgramId::Unknown);
} else {
vec.push(ProgramId::LightSystem);
}
} else if program_id == &Pubkey::from(ACCOUNT_COMPRESSION_PROGRAM_ID) {
if discriminator == DISCRIMINATOR_INSERT_INTO_QUEUES
&& accounts.len() > 2
&& accounts[1] == Pubkey::from(REGISTERED_PROGRAM_PDA)
{
vec.push(ProgramId::AccountCompression);
} else {
vec.push(ProgramId::Unknown);
}
} else if program_id == &Pubkey::from(LIGHT_TOKEN_PROGRAM_ID) {
if !instruction.is_empty() && instruction[0] == TRANSFER2 {
vec.push(ProgramId::LightToken);
} else {
vec.push(ProgramId::Unknown);
}
} else if program_id == &Pubkey::from(LIGHT_REGISTRY_PROGRAM_ID) {
vec.push(ProgramId::Registry);
} else {
vec.push(ProgramId::Unknown);
}
}
vec
}
fn deserialize_instruction<'a>(
instruction: &'a [u8],
accounts: &'a [Pubkey],
) -> Result<ExecutingSystemInstruction<'a>, ParseIndexerEventError> {
if instruction.len() < 12 {
return Err(ParseIndexerEventError::InstructionDataTooSmall(
instruction.len(),
12,
));
}
let instruction_discriminator = instruction[0..8].try_into().unwrap();
let instruction = instruction.split_at(8).1;
match instruction_discriminator {
DISCRIMINATOR_INVOKE => {
if accounts.len() < 9 {
return Err(ParseIndexerEventError::DeserializeSystemInstructionError);
}
let accounts = accounts.split_at(9).1;
let data = InstructionDataInvoke::deserialize(&mut &instruction[4..])?;
Ok(ExecutingSystemInstruction {
output_compressed_accounts: data.output_compressed_accounts,
input_compressed_accounts: data.input_compressed_accounts_with_merkle_context,
is_compress: data.is_compress,
relay_fee: data.relay_fee,
compress_or_decompress_lamports: data.compress_or_decompress_lamports,
execute_cpi_context: false,
accounts,
})
}
DISCRIMINATOR_INVOKE_CPI => {
if accounts.len() < 11 {
return Err(ParseIndexerEventError::DeserializeSystemInstructionError);
}
let accounts = accounts.split_at(11).1;
let data = light_sdk_types::lca::instruction_data::invoke_cpi::InstructionDataInvokeCpi::deserialize(
&mut &instruction[4..],
)?;
Ok(ExecutingSystemInstruction {
output_compressed_accounts: data.output_compressed_accounts,
input_compressed_accounts: data.input_compressed_accounts_with_merkle_context,
is_compress: data.is_compress,
relay_fee: data.relay_fee,
compress_or_decompress_lamports: data.compress_or_decompress_lamports,
execute_cpi_context: data.cpi_context.is_some(),
accounts,
})
}
DISCRIMINATOR_INVOKE_CPI_WITH_READ_ONLY => {
if accounts.len() < 5 {
return Err(ParseIndexerEventError::DeserializeSystemInstructionError);
}
let data: InstructionDataInvokeCpiWithReadOnly =
InstructionDataInvokeCpiWithReadOnly::deserialize(&mut &instruction[..])?;
let system_accounts_len = if data.mode == 0 {
11
} else {
let mut len = 6; if data.compress_or_decompress_lamports > 0 {
len += 1;
}
if !data.is_compress && data.compress_or_decompress_lamports > 0 {
len += 1;
}
if data.with_cpi_context {
len += 1;
}
len
};
let accounts = accounts.split_at(system_accounts_len).1;
Ok(ExecutingSystemInstruction {
output_compressed_accounts: data.output_compressed_accounts,
input_compressed_accounts: data
.input_compressed_accounts
.iter()
.map(|x| {
x.into_packed_compressed_account_with_merkle_context(
data.invoking_program_id,
)
})
.collect::<Vec<_>>(),
is_compress: data.is_compress && data.compress_or_decompress_lamports > 0,
relay_fee: None,
compress_or_decompress_lamports: if data.compress_or_decompress_lamports == 0 {
None
} else {
Some(data.compress_or_decompress_lamports)
},
execute_cpi_context: data.with_cpi_context,
accounts,
})
}
INVOKE_CPI_WITH_ACCOUNT_INFO_INSTRUCTION => {
if accounts.len() < 5 {
return Err(ParseIndexerEventError::DeserializeSystemInstructionError);
}
let data: InstructionDataInvokeCpiWithAccountInfo =
InstructionDataInvokeCpiWithAccountInfo::deserialize(&mut &instruction[..])?;
let system_accounts_len = if data.mode == 0 {
11
} else {
let mut len = 6; if data.compress_or_decompress_lamports > 0 {
len += 1;
}
if !data.is_compress && data.compress_or_decompress_lamports > 0 {
len += 1;
}
if data.with_cpi_context {
len += 1;
}
len
};
let accounts = accounts.split_at(system_accounts_len).1;
let instruction = ExecutingSystemInstruction {
output_compressed_accounts: data
.account_infos
.iter()
.filter(|x| x.output.is_some())
.map(|x| {
let account = x.output.as_ref().unwrap();
OutputCompressedAccountWithPackedContext {
compressed_account: CompressedAccount {
address: x.address,
owner: data.invoking_program_id,
lamports: account.lamports,
data: Some(CompressedAccountData {
discriminator: account.discriminator,
data: account.data.clone(),
data_hash: account.data_hash,
}),
},
merkle_tree_index: account.output_merkle_tree_index,
}
})
.collect::<Vec<_>>(),
input_compressed_accounts: data
.account_infos
.iter()
.filter(|x| x.input.is_some())
.map(|x| {
let account = x.input.as_ref().unwrap();
PackedCompressedAccountWithMerkleContext {
compressed_account: CompressedAccount {
address: x.address,
owner: data.invoking_program_id,
lamports: account.lamports,
data: Some(CompressedAccountData {
discriminator: account.discriminator,
data: vec![],
data_hash: account.data_hash,
}),
},
read_only: false,
root_index: account.root_index,
merkle_context: account.merkle_context,
}
})
.collect::<Vec<_>>(),
is_compress: data.is_compress && data.compress_or_decompress_lamports > 0,
relay_fee: None,
compress_or_decompress_lamports: if data.compress_or_decompress_lamports == 0 {
None
} else {
Some(data.compress_or_decompress_lamports)
},
execute_cpi_context: data.with_cpi_context,
accounts,
};
Ok(instruction)
}
_ => Err(ParseIndexerEventError::DeserializeSystemInstructionError),
}
}
fn create_batched_transaction_event(
associated_instructions: &AssociatedInstructions,
) -> Result<BatchPublicTransactionEvent, ParseIndexerEventError> {
let input_sequence_numbers = associated_instructions
.insert_into_queues_instruction
.input_sequence_numbers
.iter()
.map(From::from)
.filter(|x: &MerkleTreeSequenceNumber| !(*x).is_empty())
.collect::<Vec<MerkleTreeSequenceNumber>>();
let mut batched_transaction_event = BatchPublicTransactionEvent {
event: PublicTransactionEvent {
input_compressed_account_hashes: associated_instructions
.insert_into_queues_instruction
.nullifiers
.iter()
.map(|x| x.account_hash)
.collect(),
output_compressed_account_hashes: associated_instructions
.insert_into_queues_instruction
.leaves
.iter()
.map(|x| x.leaf)
.collect(),
output_compressed_accounts: [
associated_instructions.cpi_context_outputs.clone(),
associated_instructions
.executing_system_instruction
.output_compressed_accounts
.clone(),
]
.concat(),
output_leaf_indices: associated_instructions
.insert_into_queues_instruction
.output_leaf_indices
.iter()
.map(|x| u32::from(*x))
.collect(),
sequence_numbers: associated_instructions
.insert_into_queues_instruction
.output_sequence_numbers
.iter()
.map(From::from)
.filter(|x: &MerkleTreeSequenceNumber| !(*x).is_empty())
.map(|x| MerkleTreeSequenceNumberV1 {
seq: x.seq,
tree_pubkey: x.tree_pubkey,
})
.collect(),
relay_fee: associated_instructions
.executing_system_instruction
.relay_fee,
is_compress: associated_instructions
.executing_system_instruction
.is_compress,
compress_or_decompress_lamports: associated_instructions
.executing_system_instruction
.compress_or_decompress_lamports,
pubkey_array: associated_instructions
.executing_system_instruction
.accounts
.to_vec(),
message: None,
ata_owners: Default::default(),
},
tx_hash: associated_instructions
.insert_into_queues_instruction
.tx_hash,
new_addresses: associated_instructions
.insert_into_queues_instruction
.addresses
.iter()
.map(|x| NewAddress {
address: x.address,
mt_pubkey: associated_instructions.accounts[x.tree_index as usize],
queue_index: u64::MAX,
})
.collect::<Vec<_>>(),
address_sequence_numbers: associated_instructions
.insert_into_queues_instruction
.address_sequence_numbers
.iter()
.map(From::from)
.filter(|x: &MerkleTreeSequenceNumber| !(*x).is_empty())
.collect::<Vec<MerkleTreeSequenceNumber>>(),
batch_input_accounts: associated_instructions
.insert_into_queues_instruction
.nullifiers
.iter()
.filter(|x| {
input_sequence_numbers.iter().any(|y| {
y.tree_pubkey == associated_instructions.accounts[x.tree_index as usize]
})
})
.map(|n| {
Ok(BatchNullifyContext {
tx_hash: associated_instructions
.insert_into_queues_instruction
.tx_hash,
account_hash: n.account_hash,
nullifier: {
create_nullifier(
&n.account_hash,
n.leaf_index.into(),
&associated_instructions
.insert_into_queues_instruction
.tx_hash,
)?
},
nullifier_queue_index: u64::MAX,
})
})
.collect::<Result<Vec<_>, ParseIndexerEventError>>()?,
input_sequence_numbers,
};
let nullifier_queue_indices = create_nullifier_queue_indices(
associated_instructions,
batched_transaction_event.batch_input_accounts.len(),
);
batched_transaction_event
.batch_input_accounts
.iter_mut()
.zip(nullifier_queue_indices.iter())
.for_each(|(context, index)| {
context.nullifier_queue_index = *index;
});
let address_queue_indices = create_address_queue_indices(
associated_instructions,
batched_transaction_event.new_addresses.len(),
);
batched_transaction_event
.new_addresses
.iter_mut()
.zip(address_queue_indices.iter())
.for_each(|(context, index)| {
context.queue_index = *index;
});
Ok(batched_transaction_event)
}
fn create_nullifier_queue_indices(
associated_instructions: &AssociatedInstructions,
len: usize,
) -> Vec<u64> {
let input_merkle_tree_pubkeys = associated_instructions
.executing_system_instruction
.input_compressed_accounts
.iter()
.map(|x| {
associated_instructions
.executing_system_instruction
.accounts[x.merkle_context.merkle_tree_pubkey_index as usize]
})
.collect::<Vec<_>>();
let mut nullifier_queue_indices = vec![u64::MAX; len];
let mut internal_input_sequence_numbers = associated_instructions
.insert_into_queues_instruction
.input_sequence_numbers
.to_vec();
let mut batch_idx = 0usize;
for merkle_tree_pubkey in input_merkle_tree_pubkeys.iter() {
if let Some(seq) = internal_input_sequence_numbers
.iter_mut()
.find(|s| s.tree_pubkey == *merkle_tree_pubkey)
{
nullifier_queue_indices[batch_idx] = seq.seq.into();
seq.seq += 1;
batch_idx += 1;
}
}
nullifier_queue_indices
}
fn create_address_queue_indices(
associated_instructions: &AssociatedInstructions,
len: usize,
) -> Vec<u64> {
let address_merkle_tree_pubkeys = associated_instructions
.insert_into_queues_instruction
.addresses
.iter()
.map(|x| associated_instructions.accounts[x.tree_index as usize])
.collect::<Vec<_>>();
let mut address_queue_indices = vec![u64::MAX; len];
let mut internal_address_sequence_numbers = associated_instructions
.insert_into_queues_instruction
.address_sequence_numbers
.to_vec();
internal_address_sequence_numbers
.iter_mut()
.for_each(|seq| {
for (i, merkle_tree_pubkey) in address_merkle_tree_pubkeys.iter().enumerate() {
if *merkle_tree_pubkey == seq.tree_pubkey.into() {
address_queue_indices[i] = seq.seq.into();
seq.seq += 1;
}
}
});
address_queue_indices
}