diff --git a/src/args/call_handler.rs b/src/args/call_handler.rs new file mode 100644 index 0000000..f890f18 --- /dev/null +++ b/src/args/call_handler.rs @@ -0,0 +1,15 @@ +use borsh::{BorshDeserialize, BorshSerialize}; + +#[derive(BorshSerialize, BorshDeserialize)] +pub enum Context { + Commit, + Undelegate, + Standalone, +} + +#[derive(BorshSerialize, BorshDeserialize)] +pub struct CallHandlerArgs { + pub escrow_index: u8, + pub data: Vec, + pub context: Context +} diff --git a/src/args/finalize_with_handler.rs b/src/args/finalize_with_handler.rs new file mode 100644 index 0000000..e3f28ba --- /dev/null +++ b/src/args/finalize_with_handler.rs @@ -0,0 +1,7 @@ +use borsh::{BorshDeserialize, BorshSerialize}; + +#[derive(BorshSerialize, BorshDeserialize)] +pub struct FinalizeWithHookArgs { + pub escrow_index: u8, + pub data: Vec, +} diff --git a/src/args/mod.rs b/src/args/mod.rs index 0d13179..56db47e 100644 --- a/src/args/mod.rs +++ b/src/args/mod.rs @@ -1,13 +1,17 @@ +mod call_handler; mod commit_state; mod delegate; mod delegate_ephemeral_balance; +mod finalize_with_handler; mod top_up_ephemeral_balance; mod validator_claim_fees; mod whitelist_validator_for_program; +pub use call_handler::*; pub use commit_state::*; pub use delegate::*; pub use delegate_ephemeral_balance::*; +pub use finalize_with_handler::*; pub use top_up_ephemeral_balance::*; pub use validator_claim_fees::*; pub use whitelist_validator_for_program::*; diff --git a/src/consts.rs b/src/consts.rs index a60e640..2bd9669 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -9,5 +9,14 @@ pub const PROTOCOL_FEES_PERCENTAGE: u8 = 10; /// The discriminator for the external undelegate instruction. pub const EXTERNAL_UNDELEGATE_DISCRIMINATOR: [u8; 8] = [196, 28, 41, 206, 48, 37, 51, 167]; +/// The discriminator for the external hook after finalization is complete +/// For anchor: corresponds to function/instruction name delegation_program_finalize_hook +pub const EXTERNAL_FINALIZE_WITH_HOOK_DISCRIMINATOR: [u8; 8] = + [74, 203, 100, 144, 173, 103, 210, 31]; + +/// The discriminator for the external hook after finalization is complete +/// For anchor: corresponds to function/instruction name delegation_program_call_handler +pub const EXTERNAL_CALL_HANDLER_DISCRIMINATOR: [u8; 8] = [157, 197, 228, 30, 0, 80, 121, 135]; + /// The program ID of the delegation program. pub const DELEGATION_PROGRAM_ID: Pubkey = crate::id(); diff --git a/src/discriminator.rs b/src/discriminator.rs index e896fa2..bd02e76 100644 --- a/src/discriminator.rs +++ b/src/discriminator.rs @@ -33,6 +33,10 @@ pub enum DlpDiscriminator { CommitStateFromBuffer = 13, /// See [crate::processor::process_close_validator_fees_vault] for docs. CloseValidatorFeesVault = 14, + /// See [crate::processor::process_call_handler] for docs. + CallHandler = 15, + /// See [crate::processor::process_finalize_with_hook] for docs. + FinalizeWithHook = 16, } impl DlpDiscriminator { @@ -60,6 +64,8 @@ impl TryFrom<[u8; 8]> for DlpDiscriminator { 0xc => Ok(DlpDiscriminator::ProtocolClaimFees), 0xd => Ok(DlpDiscriminator::CommitStateFromBuffer), 0xe => Ok(DlpDiscriminator::CloseValidatorFeesVault), + 0xf => Ok(DlpDiscriminator::CallHandler), + // 0xf => Ok(DlpDiscriminator::FinalizeWithHook), _ => Err(ProgramError::InvalidInstructionData), } } diff --git a/src/instruction_builder/call_handler.rs b/src/instruction_builder/call_handler.rs new file mode 100644 index 0000000..d0ffa8d --- /dev/null +++ b/src/instruction_builder/call_handler.rs @@ -0,0 +1,40 @@ +use crate::args::CallHandlerArgs; +use crate::discriminator::DlpDiscriminator; +use crate::pda::{ephemeral_balance_pda_from_payer, validator_fees_vault_pda_from_validator}; +use borsh::to_vec; +use solana_program::instruction::Instruction; +use solana_program::{instruction::AccountMeta, pubkey::Pubkey}; + +/// Builds a finalize state instruction. +/// See [crate::processor::call_handler] for docs. +pub fn call_handler( + validator: Pubkey, + handler_program: Pubkey, + delegated_account: Pubkey, // TODO: rename + other_accounts: Vec, + args: CallHandlerArgs, +) -> Instruction { + let validator_fees_vault_pda = validator_fees_vault_pda_from_validator(&validator); + + // handler accounts + let escrow_account = ephemeral_balance_pda_from_payer(&delegated_account, args.escrow_index); + let mut accounts = vec![ + AccountMeta::new(validator, true), + AccountMeta::new(validator_fees_vault_pda, false), + AccountMeta::new_readonly(handler_program, false), + AccountMeta::new(delegated_account, false), + AccountMeta::new(escrow_account, false), + ]; + // append other accounts at the end + accounts.extend(other_accounts); + + Instruction { + program_id: crate::id(), + accounts, + data: [ + DlpDiscriminator::CallHandler.to_vec(), + to_vec(&args).unwrap(), + ] + .concat(), + } +} diff --git a/src/instruction_builder/finalize_with_handler.rs b/src/instruction_builder/finalize_with_handler.rs new file mode 100644 index 0000000..a063cfd --- /dev/null +++ b/src/instruction_builder/finalize_with_handler.rs @@ -0,0 +1,58 @@ +use crate::args::FinalizeWithHookArgs; +use crate::discriminator::DlpDiscriminator; +use crate::pda::{ + commit_record_pda_from_delegated_account, commit_state_pda_from_delegated_account, + delegation_metadata_pda_from_delegated_account, delegation_record_pda_from_delegated_account, + ephemeral_balance_pda_from_payer, validator_fees_vault_pda_from_validator, +}; +use borsh::to_vec; +use solana_program::instruction::Instruction; +use solana_program::system_program; +use solana_program::{instruction::AccountMeta, pubkey::Pubkey}; + +/// Builds a finalize state instruction. +/// See [crate::processor::finalize_with_handler] for docs. +pub fn finalize_with_handler( + validator: Pubkey, + delegated_account: Pubkey, + other_accounts: Vec, + handler_program: Pubkey, + args: FinalizeWithHookArgs, +) -> Instruction { + // finalize accounts + let commit_state_pda = commit_state_pda_from_delegated_account(&delegated_account); + let commit_record_pda = commit_record_pda_from_delegated_account(&delegated_account); + let delegation_record_pda = delegation_record_pda_from_delegated_account(&delegated_account); + let delegation_metadata_pda = + delegation_metadata_pda_from_delegated_account(&delegated_account); + let validator_fees_vault_pda = validator_fees_vault_pda_from_validator(&validator); + + // handler accounts + let escrow_account = ephemeral_balance_pda_from_payer(&delegated_account, args.escrow_index); + let mut accounts = vec![ + // finalize accounts + AccountMeta::new(validator, true), + AccountMeta::new(delegated_account, false), + AccountMeta::new(commit_state_pda, false), + AccountMeta::new(commit_record_pda, false), + AccountMeta::new(delegation_record_pda, false), + AccountMeta::new(delegation_metadata_pda, false), + AccountMeta::new(validator_fees_vault_pda, false), + AccountMeta::new_readonly(system_program::id(), false), + // handler accounts + AccountMeta::new_readonly(handler_program, false), + AccountMeta::new(escrow_account, false), + ]; + // append other accounts at the end + accounts.extend(other_accounts); + + Instruction { + program_id: crate::id(), + accounts, + data: [ + DlpDiscriminator::FinalizeWithHook.to_vec(), + to_vec(&args).unwrap(), + ] + .concat(), + } +} diff --git a/src/instruction_builder/mod.rs b/src/instruction_builder/mod.rs index e0cae79..9c1def1 100644 --- a/src/instruction_builder/mod.rs +++ b/src/instruction_builder/mod.rs @@ -1,11 +1,12 @@ mod close_ephemeral_balance; mod commit_state; - +mod call_handler; mod close_validator_fees_vault; mod commit_state_from_buffer; mod delegate; mod delegate_ephemeral_balance; mod finalize; +mod finalize_with_handler; mod init_protocol_fees_vault; mod init_validator_fees_vault; mod protocol_claim_fees; @@ -21,6 +22,7 @@ pub use commit_state_from_buffer::*; pub use delegate::*; pub use delegate_ephemeral_balance::*; pub use finalize::*; +pub use finalize_with_handler::*; pub use init_protocol_fees_vault::*; pub use init_validator_fees_vault::*; pub use protocol_claim_fees::*; @@ -28,3 +30,4 @@ pub use top_up_ephemeral_balance::*; pub use undelegate::*; pub use validator_claim_fees::*; pub use whitelist_validator_for_program::*; +pub use call_handler::*; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 4b1eca8..abe6977 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ #![allow(unexpected_cfgs)] // silence clippy for target_os solana and other solana program custom features +use crate::processor::process_call_handler; use solana_program::{ account_info::AccountInfo, declare_id, entrypoint::ProgramResult, msg, program_error::ProgramError, pubkey::Pubkey, @@ -93,6 +94,13 @@ pub fn process_instruction( discriminator::DlpDiscriminator::CloseValidatorFeesVault => { processor::process_close_validator_fees_vault(program_id, accounts, data)? } + discriminator::DlpDiscriminator::CallHandler => { + process_call_handler(program_id, accounts, data)? + } + discriminator::DlpDiscriminator::FinalizeWithHook => { + // processor::process_finalize_with_hook(program_id, accounts, data)? + unreachable!() + } } Ok(()) } diff --git a/src/processor/call_handler.rs b/src/processor/call_handler.rs new file mode 100644 index 0000000..b34c455 --- /dev/null +++ b/src/processor/call_handler.rs @@ -0,0 +1,98 @@ +use crate::args::{CallHandlerArgs}; +use crate::consts::{ + EXTERNAL_CALL_HANDLER_DISCRIMINATOR, +}; +use crate::ephemeral_balance_seeds_from_payer; +use crate::processor::utils::loaders::{ + load_initialized_validator_fees_vault, load_pda, load_signer, +}; + +use borsh::BorshDeserialize; +use solana_program::account_info::next_account_info; +use solana_program::account_info::AccountInfo; +use solana_program::entrypoint::ProgramResult; +use solana_program::instruction::{AccountMeta, Instruction}; +use solana_program::msg; +use solana_program::program::invoke_signed; +use solana_program::program_error::ProgramError; +use solana_program::pubkey::Pubkey; + +pub fn process_call_handler( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let args = CallHandlerArgs::try_from_slice(data)?; + + let accounts_iter = &mut accounts.iter(); + let validator = next_account_info(accounts_iter)?; + let validator_fees_vault = next_account_info(accounts_iter)?; + + let handler_program = next_account_info(accounts_iter)?; + // TODO: rename, actee or something like that + let delegated_account = next_account_info(accounts_iter)?; + let escrow_account = next_account_info(accounts_iter)?; + + // verify account is a signer + load_signer(validator, "validator")?; + // verify signer is a registered validator + load_initialized_validator_fees_vault(validator, validator_fees_vault, true)?; + + // Check if destination prgram is executable + if !handler_program.executable { + msg!( + "{} program is not executable: destination program", + handler_program.key + ); + return Err(ProgramError::InvalidAccountData); + } + // verify passed escrow_account derived from delegated_account + let escrow_seeds: &[&[u8]] = + ephemeral_balance_seeds_from_payer!(delegated_account.key, args.escrow_index); + let escrow_bump = load_pda( + escrow_account, + escrow_seeds, + &crate::id(), + true, + "ephemeral balance", + )?; + + // deduce necessary accounts for CPI + let (accounts_meta, handler_accounts): (Vec, Vec) = + [delegated_account, escrow_account] + .into_iter() + .chain(accounts_iter) + // TODO: check if we can keep it, but set is_signer false to prevent draining + .filter(|account| account.key != validator.key) + .map(|account| { + ( + AccountMeta { + pubkey: *account.key, + is_writable: account.is_writable, + is_signer: account.key == escrow_account.key, + }, + account.clone(), + ) + }) + .collect(); + msg!( + "Calling, accounts_meta.len: {}, handler_account.len: {}", + accounts_meta.len(), + handler_accounts.len() + ); + + let data = [EXTERNAL_CALL_HANDLER_DISCRIMINATOR.to_vec(), data.to_vec()].concat(); + let handler_instruction = Instruction { + program_id: *handler_program.key, + data, + accounts: accounts_meta, + }; + let bump_slice = &[escrow_bump]; + let escrow_signer_seeds = [escrow_seeds, &[bump_slice]].concat(); + + invoke_signed( + &handler_instruction, + &handler_accounts, + &[&escrow_signer_seeds], + ) +} diff --git a/src/processor/finalize_with_hook.rs b/src/processor/finalize_with_hook.rs new file mode 100644 index 0000000..9370375 --- /dev/null +++ b/src/processor/finalize_with_hook.rs @@ -0,0 +1,131 @@ +use crate::args::FinalizeWithHookArgs; +use crate::consts::EXTERNAL_FINALIZE_WITH_HOOK_DISCRIMINATOR; +use crate::discriminator::DlpDiscriminator; +use crate::ephemeral_balance_seeds_from_payer; +use crate::processor::utils::loaders::load_pda; + +use borsh::BorshDeserialize; +use solana_program::account_info::next_account_info; +use solana_program::account_info::AccountInfo; +use solana_program::entrypoint::ProgramResult; +use solana_program::instruction::{AccountMeta, Instruction}; +use solana_program::msg; +use solana_program::program::{invoke, invoke_signed}; +use solana_program::program_error::ProgramError; +use solana_program::pubkey::Pubkey; + +pub fn process_finalize_with_hook( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + const FINALIZE_ACCOUNTS_SIZE: usize = 8; + const HOOK_ACCOUNTS_SIZE: usize = 2; + + let args = FinalizeWithHookArgs::try_from_slice(data)?; + let (finalize_accounts, remaining_accounts) = if accounts.len() >= FINALIZE_ACCOUNTS_SIZE { + accounts.split_at(FINALIZE_ACCOUNTS_SIZE) + } else { + return Err(ProgramError::NotEnoughAccountKeys.into()); + }; + let (handler_accounts, remaining_accounts) = if remaining_accounts.len() >= HOOK_ACCOUNTS_SIZE { + remaining_accounts.split_at(HOOK_ACCOUNTS_SIZE) + } else { + return Err(ProgramError::NotEnoughAccountKeys.into()); + }; + + let accounts_iter = &mut handler_accounts.iter(); + let destination_program = next_account_info(accounts_iter)?; + let escrow_account = next_account_info(accounts_iter)?; + + // Check if destination prgram is executable + if !destination_program.executable { + msg!( + "{} program is not executable: destination program", + destination_program.key + ); + return Err(ProgramError::InvalidAccountData); + } + + // verify passed escrow_account derived from delegated_account + let delegated_account = finalize_accounts[1].clone(); + let escrow_seeds: &[&[u8]] = + ephemeral_balance_seeds_from_payer!(delegated_account.key, args.escrow_index); + let escrow_bump = load_pda( + escrow_account, + escrow_seeds, + &crate::id(), + true, + "ephemeral balance", + )?; + + // Finalize first + // TODO: make work & uncomment + // process_finalize(program_id, finalize_accounts, data)?; + + let [validator, delegated_account, commit_state_account, commit_record_account, delegation_record_account, delegation_metadata_account, validator_fees_vault, system_program] = + finalize_accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let finalize_ix = Instruction { + program_id: crate::id(), + accounts: vec![ + AccountMeta::new(*validator.key, true), + AccountMeta::new(*delegated_account.key, false), + AccountMeta::new(*commit_state_account.key, false), + AccountMeta::new(*commit_record_account.key, false), + AccountMeta::new(*delegation_record_account.key, false), + AccountMeta::new(*delegation_metadata_account.key, false), + AccountMeta::new(*validator_fees_vault.key, false), + AccountMeta::new_readonly(*system_program.key, false), + ], + data: DlpDiscriminator::Finalize.to_vec(), + }; + // TODO: remove + invoke(&finalize_ix, finalize_accounts)?; + + // deduce necessary accounts for CPI + let validator_account = finalize_accounts[0].clone(); + let (accounts_meta, handler_accounts): (Vec, Vec) = + [delegated_account.clone(), escrow_account.clone()] + .iter() + .chain(remaining_accounts) + .filter(|account| account.key != validator_account.key) + .map(|account| { + ( + AccountMeta { + pubkey: *account.key, + is_writable: account.is_writable, + is_signer: account.key == escrow_account.key, + }, + account.clone(), + ) + }) + .collect(); + msg!( + "Calling, accounts_meta.len: {}, handler_account.len: {}", + accounts_meta.len(), + handler_accounts.len() + ); + + let data = [ + EXTERNAL_FINALIZE_WITH_HOOK_DISCRIMINATOR.to_vec(), + data.to_vec(), + ] + .concat(); + let handler_instruction = Instruction { + program_id: *destination_program.key, + data, + accounts: accounts_meta, + }; + let bump_slice = &[escrow_bump]; + let escrow_signer_seeds = [escrow_seeds, &[bump_slice]].concat(); + + invoke_signed( + &handler_instruction, + &handler_accounts, + &[&escrow_signer_seeds], + ) +} diff --git a/src/processor/mod.rs b/src/processor/mod.rs index e9b25db..31653f2 100644 --- a/src/processor/mod.rs +++ b/src/processor/mod.rs @@ -1,3 +1,4 @@ +mod call_handler; mod close_ephemeral_balance; mod close_validator_fees_vault; mod commit_state; @@ -5,6 +6,7 @@ mod commit_state_from_buffer; mod delegate; mod delegate_ephemeral_balance; mod finalize; +mod finalize_with_hook; mod init_protocol_fees_vault; mod init_validator_fees_vault; mod protocol_claim_fees; @@ -14,6 +16,7 @@ mod utils; mod validator_claim_fees; mod whitelist_validator_for_program; +pub use call_handler::*; pub use close_ephemeral_balance::*; pub use close_validator_fees_vault::*; pub use commit_state::*; @@ -21,6 +24,7 @@ pub use commit_state_from_buffer::*; pub use delegate::*; pub use delegate_ephemeral_balance::*; pub use finalize::*; +pub use finalize_with_hook::*; pub use init_protocol_fees_vault::*; pub use init_validator_fees_vault::*; pub use protocol_claim_fees::*; diff --git a/tests/buffers/test_delegation.so b/tests/buffers/test_delegation.so index 6a76628..68dd274 100755 Binary files a/tests/buffers/test_delegation.so and b/tests/buffers/test_delegation.so differ diff --git a/tests/integration/Cargo.lock b/tests/integration/Cargo.lock index 631a4b5..d02f49f 100644 --- a/tests/integration/Cargo.lock +++ b/tests/integration/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ahash" @@ -173,9 +173,9 @@ dependencies = [ [[package]] name = "anchor-lang-idl" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31cf97b4e6f7d6144a05e435660fcf757dbc3446d38d0e2b851d11ed13625bba" +checksum = "32e8599d21995f68e296265aa5ab0c3cef582fd58afec014d01bd0bce18a4418" dependencies = [ "anchor-lang-idl-spec", "anyhow", @@ -379,9 +379,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" dependencies = [ "serde", ] @@ -449,11 +449,11 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.3" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" dependencies = [ - "borsh-derive 1.5.3", + "borsh-derive 1.5.7", "cfg_aliases", ] @@ -485,9 +485,9 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.3" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2593a3b8b938bd68373196c9832f516be11fa487ef4ae745eb282e6a56a7244" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" dependencies = [ "once_cell", "proc-macro-crate 3.2.0", @@ -966,9 +966,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", @@ -1067,9 +1067,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "memchr" @@ -1213,9 +1213,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -1565,7 +1565,7 @@ dependencies = [ "blake3", "borsh 0.10.4", "borsh 0.9.3", - "borsh 1.5.3", + "borsh 1.5.7", "bs58 0.4.0", "bv", "bytemuck", @@ -1797,20 +1797,21 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", @@ -1822,9 +1823,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1832,9 +1833,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -1845,15 +1846,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/tests/integration/programs/test-delegation/src/lib.rs b/tests/integration/programs/test-delegation/src/lib.rs index b8dc4ff..fdda7e6 100644 --- a/tests/integration/programs/test-delegation/src/lib.rs +++ b/tests/integration/programs/test-delegation/src/lib.rs @@ -10,6 +10,7 @@ pub const TEST_PDA_SEED_OTHER: &[u8] = b"test-pda-other"; #[ephemeral] #[program] pub mod test_delegation { + use ephemeral_rollups_sdk_v2::pda::ephemeral_balance_pda_from_payer; use super::*; pub fn initialize(ctx: Context) -> Result<()> { @@ -54,6 +55,51 @@ pub mod test_delegation { )?; Ok(()) } + + /// Handler for post commit action + pub fn delegation_program_finalize_hook(ctx: Context, hook_args: delegation_program_utils::FinalizeWithHookArgs) -> Result<()> { + let expected = ephemeral_balance_pda_from_payer(ctx.accounts.delegated_account.key, hook_args.escrow_index); + if &expected != ctx.accounts.escrow_account.key { + Err(ProgramError::InvalidAccountData) + } else { + Ok(()) + }?; + + if !ctx.accounts.escrow_account.is_signer { + Err(ProgramError::MissingRequiredSignature) + } else { + Ok(()) + }?; + + // some action here + // ... + + Ok(()) + } + + /// Delegation program call handler + pub fn delegation_program_call_handler(ctx: Context, hook_args: delegation_program_utils::CallHandlerArgs) -> Result<()> { + let expected = ephemeral_balance_pda_from_payer(ctx.accounts.delegated_account.key, hook_args.escrow_index); + if &expected != ctx.accounts.escrow_account.key { + Err(ProgramError::InvalidAccountData) + } else { + Ok(()) + }?; + + if !ctx.accounts.escrow_account.is_signer { + Err(ProgramError::MissingRequiredSignature) + } else { + Ok(()) + }?; + + match hook_args.context { + delegation_program_utils::Context::Commit => msg!("commit context"), + delegation_program_utils::Context::Undelegate => msg!("undelegate context"), + delegation_program_utils::Context::Standalone => msg!("standalone context"), + } + + Ok(()) + } } #[delegate] @@ -101,7 +147,63 @@ pub struct Increment<'info> { pub counter: Account<'info, Counter>, } +#[derive(Accounts)] +#[instruction(hook_args: delegation_program_utils::FinalizeWithHookArgs)] +pub struct DelegationProgramFinalizeHook<'info> { + pub delegated_account: UncheckedAccount<'info>, + #[account( + mut, + seeds = [b"balance", &delegated_account.key().as_ref(), &[hook_args.escrow_index]], + seeds::program = delegation_program_utils::ID, + bump + )] + pub escrow_account: Signer<'info>, + #[account(mut)] + pub destination_account: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction(hook_args: delegation_program_utils::CallHandlerArgs)] +pub struct DelegationProgramCallHandler<'info> { + pub delegated_account: UncheckedAccount<'info>, + #[account( + mut, + seeds = [b"balance", &delegated_account.key().as_ref(), &[hook_args.escrow_index]], + seeds::program = delegation_program_utils::ID, + bump + )] + pub escrow_account: Signer<'info>, + #[account(mut)] + pub destination_account: AccountInfo<'info>, +} + #[account] pub struct Counter { pub count: u64, } + +mod delegation_program_utils { + use anchor_lang::prelude::*; + + declare_id!("DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh"); + + #[derive(AnchorSerialize, AnchorDeserialize)] + pub struct FinalizeWithHookArgs { + pub escrow_index: u8, + pub data: Vec, + } + + #[derive(AnchorSerialize, AnchorDeserialize)] + pub enum Context { + Commit, + Undelegate, + Standalone, + } + + #[derive(AnchorSerialize, AnchorDeserialize)] + pub struct CallHandlerArgs { + pub escrow_index: u8, + pub data: Vec, + pub context: Context + } +} diff --git a/tests/test_call_handler.rs b/tests/test_call_handler.rs new file mode 100644 index 0000000..96ad241 --- /dev/null +++ b/tests/test_call_handler.rs @@ -0,0 +1,223 @@ +use crate::fixtures::{ + create_delegation_metadata_data, create_delegation_record_data, get_commit_record_account_data, + get_delegation_metadata_data, get_delegation_record_data, COMMIT_NEW_STATE_ACCOUNT_DATA, + DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, TEST_AUTHORITY, +}; +use dlp::args::{CallHandlerArgs, FinalizeWithHookArgs}; +use dlp::ephemeral_balance_seeds_from_payer; +use dlp::pda::{ + commit_record_pda_from_delegated_account, commit_state_pda_from_delegated_account, + delegation_metadata_pda_from_delegated_account, delegation_record_pda_from_delegated_account, + ephemeral_balance_pda_from_payer, validator_fees_vault_pda_from_validator, +}; +use solana_program::instruction::AccountMeta; +use solana_program::rent::Rent; +use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; +use solana_program_test::{processor, read_file, BanksClient, ProgramTest}; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::{ + account::Account, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +mod fixtures; + +async fn setup_delegated_pda(program_test: &mut ProgramTest, authority_pubkey: &Pubkey) { + // Setup a delegated PDA + program_test.add_account( + DELEGATED_PDA_ID, + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegation record PDA + let delegation_record_data = get_delegation_record_data(*authority_pubkey, None); + program_test.add_account( + delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default().minimum_balance(delegation_record_data.len()), + data: delegation_record_data.clone(), + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated account metadata PDA + let delegation_metadata_data = get_delegation_metadata_data(*authority_pubkey, None); + program_test.add_account( + delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default().minimum_balance(delegation_metadata_data.len()), + data: delegation_metadata_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +async fn setup_commit_state(program_test: &mut ProgramTest, authority_pubkey: &Pubkey) { + // Setup the commit state PDA + program_test.add_account( + commit_state_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: LAMPORTS_PER_SOL, + data: COMMIT_NEW_STATE_ACCOUNT_DATA.into(), + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let commit_record_data = get_commit_record_account_data(*authority_pubkey); + program_test.add_account( + commit_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default().minimum_balance(commit_record_data.len()), + data: commit_record_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +async fn setup_escrow_account(program_test: &mut ProgramTest, authority_pubkey: &Pubkey) { + let ephemeral_balance_pda = ephemeral_balance_pda_from_payer(&DELEGATED_PDA_ID, 0); + + // Setup the delegated account PDA + program_test.add_account( + ephemeral_balance_pda, + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated record PDA + let delegation_record_data = + create_delegation_record_data(*authority_pubkey, dlp::id(), Some(LAMPORTS_PER_SOL)); + program_test.add_account( + delegation_record_pda_from_delegated_account(&ephemeral_balance_pda), + Account { + lamports: Rent::default().minimum_balance(delegation_record_data.len()), + data: delegation_record_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated account metadata PDA + let delegation_metadata_data = create_delegation_metadata_data( + *authority_pubkey, + ephemeral_balance_seeds_from_payer!(DELEGATED_PDA_ID, 0), + true, + ); + program_test.add_account( + delegation_metadata_pda_from_delegated_account(&ephemeral_balance_pda), + Account { + lamports: Rent::default().minimum_balance(delegation_metadata_data.len()), + data: delegation_metadata_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { + let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + program_test.prefer_bpf(true); + + let authority = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); + + // Setup authority + program_test.add_account( + authority.pubkey(), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup necessary accounts + setup_delegated_pda(&mut program_test, &authority.pubkey()).await; + setup_commit_state(&mut program_test, &authority.pubkey()).await; + setup_escrow_account(&mut program_test, &authority.pubkey()).await; + + // Setup the validator fees vault + program_test.add_account( + validator_fees_vault_pda_from_validator(&authority.pubkey()), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup program to test delegation + let data = read_file("tests/buffers/test_delegation.so"); + program_test.add_account( + DELEGATED_PDA_OWNER_ID, + Account { + lamports: Rent::default().minimum_balance(data.len()).max(1), + data, + owner: solana_sdk::bpf_loader::id(), + executable: true, + rent_epoch: 0, + }, + ); + + let (banks, payer, blockhash) = program_test.start().await; + (banks, payer, authority, blockhash) +} + +/// Testing call_handler in finalize context +#[tokio::test] +async fn test_finalize_call_handler() { + // Setup + let (banks, _, authority, blockhash) = setup_program_test_env().await; + + // Submit the finalize with handler tx + let destination = Keypair::new(); + let finalize_ix = dlp::instruction_builder::finalize( + authority.pubkey(), + DELEGATED_PDA_ID + ); + let call_handler_ix = dlp::instruction_builder::call_handler( + authority.pubkey(), + DELEGATED_PDA_OWNER_ID, // handler program + DELEGATED_PDA_ID, + vec![AccountMeta::new(destination.pubkey(), false)], + CallHandlerArgs { + escrow_index: 0, + data: vec![], + context: dlp::args::Context::Commit, + } + ); + let tx = Transaction::new_signed_with_payer( + &[finalize_ix, call_handler_ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + println!("{:?}", res); + assert!(res.is_ok()); +} diff --git a/tests/test_finalize_with_data.rs b/tests/test_finalize_with_data.rs new file mode 100644 index 0000000..402fa9a --- /dev/null +++ b/tests/test_finalize_with_data.rs @@ -0,0 +1,217 @@ +use crate::fixtures::{ + create_delegation_metadata_data, create_delegation_record_data, get_commit_record_account_data, + get_delegation_metadata_data, get_delegation_record_data, COMMIT_NEW_STATE_ACCOUNT_DATA, + DELEGATED_PDA_ID, DELEGATED_PDA_OWNER_ID, TEST_AUTHORITY, +}; +use dlp::args::FinalizeWithHookArgs; +use dlp::ephemeral_balance_seeds_from_payer; +use dlp::pda::{ + commit_record_pda_from_delegated_account, commit_state_pda_from_delegated_account, + delegation_metadata_pda_from_delegated_account, delegation_record_pda_from_delegated_account, + ephemeral_balance_pda_from_payer, validator_fees_vault_pda_from_validator, +}; +use solana_program::instruction::AccountMeta; +use solana_program::rent::Rent; +use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; +use solana_program_test::{processor, read_file, BanksClient, ProgramTest}; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::{ + account::Account, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +mod fixtures; + +async fn setup_delegated_pda(program_test: &mut ProgramTest, authority_pubkey: &Pubkey) { + // Setup a delegated PDA + program_test.add_account( + DELEGATED_PDA_ID, + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegation record PDA + let delegation_record_data = get_delegation_record_data(*authority_pubkey, None); + program_test.add_account( + delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default().minimum_balance(delegation_record_data.len()), + data: delegation_record_data.clone(), + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated account metadata PDA + let delegation_metadata_data = get_delegation_metadata_data(*authority_pubkey, None); + program_test.add_account( + delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default().minimum_balance(delegation_metadata_data.len()), + data: delegation_metadata_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +async fn setup_commit_state(program_test: &mut ProgramTest, authority_pubkey: &Pubkey) { + // Setup the commit state PDA + program_test.add_account( + commit_state_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: LAMPORTS_PER_SOL, + data: COMMIT_NEW_STATE_ACCOUNT_DATA.into(), + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let commit_record_data = get_commit_record_account_data(*authority_pubkey); + program_test.add_account( + commit_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default().minimum_balance(commit_record_data.len()), + data: commit_record_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +async fn setup_escrow_account(program_test: &mut ProgramTest, authority_pubkey: &Pubkey) { + let ephemeral_balance_pda = ephemeral_balance_pda_from_payer(&DELEGATED_PDA_ID, 0); + + // Setup the delegated account PDA + program_test.add_account( + ephemeral_balance_pda, + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated record PDA + let delegation_record_data = + create_delegation_record_data(*authority_pubkey, dlp::id(), Some(LAMPORTS_PER_SOL)); + program_test.add_account( + delegation_record_pda_from_delegated_account(&ephemeral_balance_pda), + Account { + lamports: Rent::default().minimum_balance(delegation_record_data.len()), + data: delegation_record_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated account metadata PDA + let delegation_metadata_data = create_delegation_metadata_data( + *authority_pubkey, + ephemeral_balance_seeds_from_payer!(DELEGATED_PDA_ID, 0), + true, + ); + program_test.add_account( + delegation_metadata_pda_from_delegated_account(&ephemeral_balance_pda), + Account { + lamports: Rent::default().minimum_balance(delegation_metadata_data.len()), + data: delegation_metadata_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { + let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + program_test.prefer_bpf(true); + + let authority = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); + + // Setup authority + program_test.add_account( + authority.pubkey(), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup necessary accounts + setup_delegated_pda(&mut program_test, &authority.pubkey()).await; + setup_commit_state(&mut program_test, &authority.pubkey()).await; + setup_escrow_account(&mut program_test, &authority.pubkey()).await; + + // Setup the validator fees vault + program_test.add_account( + validator_fees_vault_pda_from_validator(&authority.pubkey()), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup program to test delegation + let data = read_file("tests/buffers/test_delegation.so"); + program_test.add_account( + DELEGATED_PDA_OWNER_ID, + Account { + lamports: Rent::default().minimum_balance(data.len()).max(1), + data, + owner: solana_sdk::bpf_loader::id(), + executable: true, + rent_epoch: 0, + }, + ); + + let (banks, payer, blockhash) = program_test.start().await; + (banks, payer, authority, blockhash) +} + +#[tokio::test] +async fn test_finalize_with_data() { + // Setup + let (banks, _, authority, blockhash) = setup_program_test_env().await; + + // Submit the finalize with handler tx + let destination = Keypair::new(); + let ix = dlp::instruction_builder::finalize_with_handler( + authority.pubkey(), + DELEGATED_PDA_ID, + vec![AccountMeta::new(destination.pubkey(), false)], + DELEGATED_PDA_OWNER_ID, + FinalizeWithHookArgs { + escrow_index: 0, + data: vec![], + }, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + println!("{:?}", res); + assert!(res.is_ok()); +}