use borsh::{BorshDeserialize, BorshSerialize};
use solana_pubkey::Pubkey;
use solana_sdk::instruction::{AccountMeta, Instruction};
pub const REGISTRY_PROGRAM_ID: Pubkey =
solana_pubkey::pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX");
pub const FORESTER_SEED: &[u8] = b"forester";
pub const FORESTER_EPOCH_SEED: &[u8] = b"forester_epoch";
pub const PROTOCOL_CONFIG_PDA_SEED: &[u8] = b"authority";
pub const CLAIM_DISCRIMINATOR: [u8; 8] = [62, 198, 214, 193, 213, 159, 108, 210];
pub const COMPRESS_AND_CLOSE_DISCRIMINATOR: [u8; 8] = [96, 94, 135, 18, 121, 42, 213, 117];
pub const REGISTER_FORESTER_DISCRIMINATOR: [u8; 8] = [62, 47, 240, 103, 84, 200, 226, 73];
pub const REGISTER_FORESTER_EPOCH_DISCRIMINATOR: [u8; 8] = [43, 120, 253, 194, 109, 192, 101, 188];
pub const FINALIZE_REGISTRATION_DISCRIMINATOR: [u8; 8] = [230, 188, 172, 96, 204, 247, 98, 227];
#[allow(dead_code)]
pub const REPORT_WORK_DISCRIMINATOR: [u8; 8] = [170, 110, 232, 47, 145, 213, 138, 162];
pub const PROTOCOL_CONFIG_PDA_DISCRIMINATOR: [u8; 8] = [96, 176, 239, 146, 1, 254, 99, 146];
pub const FORESTER_PDA_DISCRIMINATOR: [u8; 8] = [51, 47, 187, 86, 82, 153, 117, 5];
pub const FORESTER_EPOCH_PDA_DISCRIMINATOR: [u8; 8] = [29, 117, 211, 141, 99, 143, 250, 114];
pub const EPOCH_PDA_DISCRIMINATOR: [u8; 8] = [66, 224, 46, 2, 167, 137, 120, 107];
pub fn get_protocol_config_pda_address() -> (Pubkey, u8) {
Pubkey::find_program_address(&[PROTOCOL_CONFIG_PDA_SEED], ®ISTRY_PROGRAM_ID)
}
pub fn get_forester_pda(authority: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[FORESTER_SEED, authority.as_ref()], ®ISTRY_PROGRAM_ID)
}
pub fn get_forester_epoch_pda(forester_pda: &Pubkey, epoch: u64) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[
FORESTER_EPOCH_SEED,
forester_pda.as_ref(),
epoch.to_le_bytes().as_slice(),
],
®ISTRY_PROGRAM_ID,
)
}
pub fn get_forester_epoch_pda_from_authority(authority: &Pubkey, epoch: u64) -> (Pubkey, u8) {
let forester_pda = get_forester_pda(authority);
get_forester_epoch_pda(&forester_pda.0, epoch)
}
pub fn get_epoch_pda_address(epoch: u64) -> Pubkey {
Pubkey::find_program_address(&[&epoch.to_le_bytes()], ®ISTRY_PROGRAM_ID).0
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct ForesterConfig {
pub fee: u64,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct ForesterPda {
pub authority: Pubkey,
pub config: ForesterConfig,
pub active_weight: u64,
pub pending_weight: u64,
pub current_epoch: u64,
pub last_compressed_forester_epoch_pda_hash: [u8; 32],
pub last_registered_epoch: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct ProtocolConfig {
pub genesis_slot: u64,
pub min_weight: u64,
pub slot_length: u64,
pub registration_phase_length: u64,
pub active_phase_length: u64,
pub report_work_phase_length: u64,
pub network_fee: u64,
pub cpi_context_size: u64,
pub finalize_counter_limit: u64,
pub place_holder: Pubkey,
pub address_network_fee: u64,
pub place_holder_b: u64,
pub place_holder_c: u64,
pub place_holder_d: u64,
pub place_holder_e: u64,
pub place_holder_f: u64,
}
impl Default for ProtocolConfig {
fn default() -> Self {
Self {
genesis_slot: 0,
min_weight: 1,
slot_length: 10,
registration_phase_length: 100,
active_phase_length: 1000,
report_work_phase_length: 100,
network_fee: 5000,
cpi_context_size: 20 * 1024 + 8, finalize_counter_limit: 100,
place_holder: Pubkey::default(),
address_network_fee: 10000,
place_holder_b: 0,
place_holder_c: 0,
place_holder_d: 0,
place_holder_e: 0,
place_holder_f: 0,
}
}
}
#[derive(Debug, BorshDeserialize)]
pub struct ProtocolConfigPda {
pub authority: Pubkey,
pub bump: u8,
pub config: ProtocolConfig,
}
#[derive(Debug, Copy, Clone, BorshSerialize, BorshDeserialize)]
pub struct CompressAndCloseIndices {
pub source_index: u8,
pub mint_index: u8,
pub owner_index: u8,
pub rent_sponsor_index: u8,
pub delegate_index: u8,
}
pub fn build_claim_instruction(
authority: Pubkey,
registered_forester_pda: Pubkey,
rent_sponsor: Pubkey,
compression_authority: Pubkey,
compressible_config: Pubkey,
compressed_token_program: Pubkey,
token_accounts: &[Pubkey],
) -> Instruction {
let mut accounts = vec![
AccountMeta::new(authority, true),
AccountMeta::new(registered_forester_pda, false),
AccountMeta::new(rent_sponsor, false),
AccountMeta::new_readonly(compression_authority, false),
AccountMeta::new_readonly(compressible_config, false),
AccountMeta::new_readonly(compressed_token_program, false),
];
for token_account in token_accounts {
accounts.push(AccountMeta::new(*token_account, false));
}
Instruction {
program_id: REGISTRY_PROGRAM_ID,
accounts,
data: CLAIM_DISCRIMINATOR.to_vec(),
}
}
#[allow(clippy::too_many_arguments)]
pub fn build_compress_and_close_instruction(
authority: Pubkey,
registered_forester_pda: Pubkey,
compression_authority: Pubkey,
compressible_config: Pubkey,
authority_index: u8,
destination_index: u8,
indices: Vec<CompressAndCloseIndices>,
remaining_accounts: Vec<AccountMeta>,
) -> Instruction {
let mut accounts = vec![
AccountMeta::new(authority, true),
AccountMeta::new(registered_forester_pda, false),
AccountMeta::new(compression_authority, false),
AccountMeta::new_readonly(compressible_config, false),
];
accounts.extend(remaining_accounts);
let mut data = COMPRESS_AND_CLOSE_DISCRIMINATOR.to_vec();
data.push(authority_index);
data.push(destination_index);
indices.serialize(&mut data).unwrap();
Instruction {
program_id: REGISTRY_PROGRAM_ID,
accounts,
data,
}
}
pub fn create_register_forester_instruction(
fee_payer: &Pubkey,
governance_authority: &Pubkey,
forester_authority: &Pubkey,
config: ForesterConfig,
) -> Instruction {
let (forester_pda, bump) = get_forester_pda(forester_authority);
let (protocol_config_pda, _) = get_protocol_config_pda_address();
let accounts = vec![
AccountMeta::new(*fee_payer, true),
AccountMeta::new_readonly(*governance_authority, true),
AccountMeta::new_readonly(protocol_config_pda, false),
AccountMeta::new(forester_pda, false),
AccountMeta::new_readonly(solana_sdk::system_program::id(), false),
];
let mut data = REGISTER_FORESTER_DISCRIMINATOR.to_vec();
data.push(bump);
data.extend_from_slice(forester_authority.as_ref());
config.serialize(&mut data).unwrap();
data.push(1u8); data.extend_from_slice(&1u64.to_le_bytes());
Instruction {
program_id: REGISTRY_PROGRAM_ID,
accounts,
data,
}
}
pub fn create_register_forester_epoch_pda_instruction(
authority: &Pubkey,
derivation: &Pubkey,
epoch: u64,
) -> Instruction {
let (forester_epoch_pda, _bump) = get_forester_epoch_pda_from_authority(derivation, epoch);
let (forester_pda, _) = get_forester_pda(derivation);
let epoch_pda = get_epoch_pda_address(epoch);
let protocol_config_pda = get_protocol_config_pda_address().0;
let accounts = vec![
AccountMeta::new(*authority, true), AccountMeta::new(forester_epoch_pda, false), AccountMeta::new_readonly(forester_pda, false), AccountMeta::new_readonly(*authority, true), AccountMeta::new(epoch_pda, false), AccountMeta::new_readonly(protocol_config_pda, false), AccountMeta::new_readonly(solana_sdk::system_program::id(), false), ];
let mut data = REGISTER_FORESTER_EPOCH_DISCRIMINATOR.to_vec();
data.extend_from_slice(&epoch.to_le_bytes());
Instruction {
program_id: REGISTRY_PROGRAM_ID,
accounts,
data,
}
}
pub fn create_finalize_registration_instruction(
authority: &Pubkey,
derivation: &Pubkey,
epoch: u64,
) -> Instruction {
let (forester_epoch_pda, _bump) = get_forester_epoch_pda_from_authority(derivation, epoch);
let epoch_pda = get_epoch_pda_address(epoch);
let accounts = vec![
AccountMeta::new(forester_epoch_pda, false),
AccountMeta::new_readonly(*authority, true),
AccountMeta::new_readonly(epoch_pda, false),
];
Instruction {
program_id: REGISTRY_PROGRAM_ID,
accounts,
data: FINALIZE_REGISTRATION_DISCRIMINATOR.to_vec(),
}
}
pub fn deserialize_protocol_config_pda(data: &[u8]) -> Result<ProtocolConfigPda, std::io::Error> {
if data.len() < 8 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Account data too short for discriminator",
));
}
ProtocolConfigPda::deserialize(&mut &data[8..])
}
pub fn deserialize_forester_pda(data: &[u8]) -> Result<ForesterPda, std::io::Error> {
if data.len() < 8 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Account data too short for discriminator",
));
}
ForesterPda::deserialize(&mut &data[8..])
}
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct ForesterEpochPda {
pub authority: Pubkey,
pub config: ForesterConfig,
pub epoch: u64,
pub weight: u64,
pub work_counter: u64,
pub has_reported_work: bool,
pub forester_index: u64,
pub epoch_active_phase_start_slot: u64,
pub total_epoch_weight: Option<u64>,
pub protocol_config: ProtocolConfig,
pub finalize_counter: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct EpochPda {
pub epoch: u64,
pub protocol_config: ProtocolConfig,
pub total_work: u64,
pub registered_weight: u64,
}
pub fn protocol_config_for_tests() -> ProtocolConfig {
ProtocolConfig {
genesis_slot: 0,
min_weight: 1,
slot_length: 10,
registration_phase_length: 0, active_phase_length: u64::MAX / 2, report_work_phase_length: 0,
network_fee: 5000,
cpi_context_size: 20 * 1024 + 8,
finalize_counter_limit: u64::MAX,
place_holder: Pubkey::default(),
address_network_fee: 10000,
place_holder_b: 0,
place_holder_c: 0,
place_holder_d: 0,
place_holder_e: 0,
place_holder_f: 0,
}
}
pub fn serialize_protocol_config_pda(
authority: Pubkey,
bump: u8,
config: ProtocolConfig,
) -> Vec<u8> {
let mut data = PROTOCOL_CONFIG_PDA_DISCRIMINATOR.to_vec();
authority.serialize(&mut data).unwrap();
data.push(bump);
config.serialize(&mut data).unwrap();
data
}
pub fn serialize_forester_pda(forester: &ForesterPda) -> Vec<u8> {
let mut data = FORESTER_PDA_DISCRIMINATOR.to_vec();
forester.authority.serialize(&mut data).unwrap();
forester.config.serialize(&mut data).unwrap();
forester.active_weight.serialize(&mut data).unwrap();
forester.pending_weight.serialize(&mut data).unwrap();
forester.current_epoch.serialize(&mut data).unwrap();
forester
.last_compressed_forester_epoch_pda_hash
.serialize(&mut data)
.unwrap();
forester.last_registered_epoch.serialize(&mut data).unwrap();
data
}
pub fn serialize_forester_epoch_pda(epoch_pda: &ForesterEpochPda) -> Vec<u8> {
let mut data = FORESTER_EPOCH_PDA_DISCRIMINATOR.to_vec();
epoch_pda.serialize(&mut data).unwrap();
data
}
pub fn serialize_epoch_pda(epoch_pda: &EpochPda) -> Vec<u8> {
let mut data = EPOCH_PDA_DISCRIMINATOR.to_vec();
epoch_pda.serialize(&mut data).unwrap();
data
}
pub fn setup_test_protocol_accounts(
context: &mut litesvm::LiteSVM,
forester_authority: &Pubkey,
) -> Result<(), String> {
let protocol_config = protocol_config_for_tests();
let (protocol_config_pda, protocol_bump) = get_protocol_config_pda_address();
let protocol_data = serialize_protocol_config_pda(
*forester_authority, protocol_bump,
protocol_config,
);
let protocol_account = solana_account::Account {
lamports: 1_000_000_000,
data: protocol_data,
owner: REGISTRY_PROGRAM_ID,
executable: false,
rent_epoch: 0,
};
context
.set_account(protocol_config_pda, protocol_account)
.map_err(|e| format!("Failed to set protocol config account: {}", e))?;
let (forester_pda, _forester_bump) = get_forester_pda(forester_authority);
let forester = ForesterPda {
authority: *forester_authority,
config: ForesterConfig::default(),
active_weight: 1,
pending_weight: 0,
current_epoch: 0,
last_compressed_forester_epoch_pda_hash: [0u8; 32],
last_registered_epoch: 0,
};
let forester_data = serialize_forester_pda(&forester);
let forester_account = solana_account::Account {
lamports: 1_000_000_000,
data: forester_data,
owner: REGISTRY_PROGRAM_ID,
executable: false,
rent_epoch: 0,
};
context
.set_account(forester_pda, forester_account)
.map_err(|e| format!("Failed to set forester account: {}", e))?;
let (forester_epoch_pda, _epoch_bump) =
get_forester_epoch_pda_from_authority(forester_authority, 0);
let forester_epoch = ForesterEpochPda {
authority: *forester_authority,
config: ForesterConfig::default(),
epoch: 0,
weight: 1,
work_counter: 0,
has_reported_work: false,
forester_index: 0,
epoch_active_phase_start_slot: 0,
total_epoch_weight: Some(1), protocol_config,
finalize_counter: 1, };
let forester_epoch_data = serialize_forester_epoch_pda(&forester_epoch);
let forester_epoch_account = solana_account::Account {
lamports: 1_000_000_000,
data: forester_epoch_data,
owner: REGISTRY_PROGRAM_ID,
executable: false,
rent_epoch: 0,
};
context
.set_account(forester_epoch_pda, forester_epoch_account)
.map_err(|e| format!("Failed to set forester epoch account: {}", e))?;
let epoch_pda_address = get_epoch_pda_address(0);
let epoch_pda = EpochPda {
epoch: 0,
protocol_config,
total_work: 0,
registered_weight: 1, };
let epoch_pda_data = serialize_epoch_pda(&epoch_pda);
let epoch_pda_account = solana_account::Account {
lamports: 1_000_000_000,
data: epoch_pda_data,
owner: REGISTRY_PROGRAM_ID,
executable: false,
rent_epoch: 0,
};
context
.set_account(epoch_pda_address, epoch_pda_account)
.map_err(|e| format!("Failed to set epoch pda account: {}", e))?;
Ok(())
}