From 4502c6dcf9d8c1686d4ab48e5c013f1de2c9a6d1 Mon Sep 17 00:00:00 2001 From: swimricky <86628128+swimricky@users.noreply.github.com> Date: Wed, 5 Apr 2023 16:33:12 -0700 Subject: [PATCH] [accumulator-updater 3/x] Update Inputs Ix (#741) * feat(accumulator_updater): initial skeleton for accumulator_udpater program Initial layout for accumulator updater program. Includes mock-cpi-caller which is meant to represent pyth oracle(or any future allowed program) that will call the accumulator updater program. All implementation details are open for discussion/subject to change * test(accumulator_updater): add additional checks in tests and minor clean up * chore(accumulator_updater): misc clean-up * refactor(accumulator_updater): added comments & to-dos and minor refactoring to address PR comments * feat(accumulator_updater): nmanual serialization for mock-cpi-caller schemas & use zero-copy * chore(accumulator-updater): misc clean-up * refactor(accumulator-updater): rename parameter in accumulator-updater::initalize ix for consistency * style(accumulator-updater): switch PriceAccountType enum variants to camelcase * refactor(accumulator-updater): address PR comments rename schema to message & associated price messages, remove unncessary comments, changed addAllowedProgram to setAllowedPrograms * refactor(accumulator-updater): address more PR comments consolidate SetAllowedPrograms and UpdateWhitelistAuthority into one context * style(accumulator-updater): minor style fixes to address PR comments * feat(accumulator-updater): implement update-inputs ix, add bump to AccumulatorInput header * feat(accumulator-updater): implement update ix * refactor(accumulator-updater): refactor ixs & state into modules * docs(accumulator-updater): add comments for ixs * feat(accumulator-updater): consolidate add/update_inputs into emit_inputs ix (#743) * refactor(accumulator-updater): clean up unused fn, rename fns * fix(accumulator-updater): fix error from resolving merge conflicts * refactor(accumulator_updater): address PR comments Remove account_type, rename emit_inputs to put_all to be more similar to a Map interface, added AccumulatorHeader::CURRENT_VERSION * docs(accumulator_updater): add docs for AccumulatorInput struct --- .../src/instructions/mod.rs | 3 + .../src/instructions/put_all.rs | 150 ++++++++++ .../programs/accumulator_updater/src/lib.rs | 282 +++--------------- .../accumulator_updater/src/macros.rs | 21 +- .../src/state/accumulator_input.rs | 90 ++++++ .../accumulator_updater/src/state/mod.rs | 7 + .../src/state/whitelist.rs | 70 +++++ .../src/instructions/add_price.rs | 134 +++++++++ .../mock-cpi-caller/src/instructions/mod.rs | 23 ++ .../src/instructions/update_price.rs | 134 +++++++++ .../programs/mock-cpi-caller/src/lib.rs | 237 +-------------- .../programs/mock-cpi-caller/src/message.rs | 2 +- .../mock-cpi-caller/src/message/price.rs | 2 +- .../programs/mock-cpi-caller/src/state/mod.rs | 25 ++ .../mock-cpi-caller/src/state/price.rs | 47 +++ .../tests/accumulator_updater.ts | 152 ++++++++-- 16 files changed, 867 insertions(+), 512 deletions(-) create mode 100644 accumulator_updater/programs/accumulator_updater/src/instructions/mod.rs create mode 100644 accumulator_updater/programs/accumulator_updater/src/instructions/put_all.rs create mode 100644 accumulator_updater/programs/accumulator_updater/src/state/accumulator_input.rs create mode 100644 accumulator_updater/programs/accumulator_updater/src/state/mod.rs create mode 100644 accumulator_updater/programs/accumulator_updater/src/state/whitelist.rs create mode 100644 accumulator_updater/programs/mock-cpi-caller/src/instructions/add_price.rs create mode 100644 accumulator_updater/programs/mock-cpi-caller/src/instructions/mod.rs create mode 100644 accumulator_updater/programs/mock-cpi-caller/src/instructions/update_price.rs create mode 100644 accumulator_updater/programs/mock-cpi-caller/src/state/mod.rs create mode 100644 accumulator_updater/programs/mock-cpi-caller/src/state/price.rs diff --git a/accumulator_updater/programs/accumulator_updater/src/instructions/mod.rs b/accumulator_updater/programs/accumulator_updater/src/instructions/mod.rs new file mode 100644 index 00000000..7c9857a8 --- /dev/null +++ b/accumulator_updater/programs/accumulator_updater/src/instructions/mod.rs @@ -0,0 +1,3 @@ +pub use put_all::*; + +mod put_all; diff --git a/accumulator_updater/programs/accumulator_updater/src/instructions/put_all.rs b/accumulator_updater/programs/accumulator_updater/src/instructions/put_all.rs new file mode 100644 index 00000000..9e145e17 --- /dev/null +++ b/accumulator_updater/programs/accumulator_updater/src/instructions/put_all.rs @@ -0,0 +1,150 @@ +use { + crate::{ + state::*, + AccumulatorUpdaterError, + }, + anchor_lang::{ + prelude::*, + system_program::{ + self, + CreateAccount, + }, + }, +}; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct InputSchemaAndData { + pub schema: u8, + pub data: Vec, +} + + +pub fn put_all<'info>( + ctx: Context<'_, '_, '_, 'info, PutAll<'info>>, + base_account_key: Pubkey, + values: Vec, +) -> Result<()> { + let cpi_caller = ctx.accounts.whitelist_verifier.is_allowed()?; + let account_infos = ctx.remaining_accounts; + require_eq!(account_infos.len(), values.len()); + + let rent = Rent::get()?; + let (mut initialized, mut updated) = (vec![], vec![]); + + for ( + ai, + InputSchemaAndData { + schema: account_schema, + data: account_data, + }, + ) in account_infos.iter().zip(values) + { + let bump = if is_uninitialized_account(ai) { + let seeds = &[ + cpi_caller.as_ref(), + b"accumulator".as_ref(), + base_account_key.as_ref(), + &account_schema.to_le_bytes(), + ]; + let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); + require_keys_eq!(ai.key(), pda); + + + //TODO: Update this with serialization logic + // 8 for anchor discriminator + let accumulator_size = 8 + AccumulatorInput::size(&account_data); + PutAll::create_account( + ai, + accumulator_size, + &ctx.accounts.payer, + &[ + cpi_caller.as_ref(), + b"accumulator".as_ref(), + base_account_key.as_ref(), + &account_schema.to_le_bytes(), + &[bump], + ], + &rent, + &ctx.accounts.system_program, + )?; + initialized.push(ai.key()); + + bump + } else { + let accumulator_input = ::try_deserialize( + &mut &**ai.try_borrow_mut_data()?, + )?; + { + // TODO: allow re-sizing? + require_gte!( + accumulator_input.data.len(), + account_data.len(), + AccumulatorUpdaterError::CurrentDataLengthExceeded + ); + + accumulator_input.validate( + ai.key(), + cpi_caller, + base_account_key, + account_schema, + )?; + } + + + updated.push(ai.key()); + accumulator_input.header.bump + }; + + let accumulator_input = + AccumulatorInput::new(AccumulatorHeader::new(bump, account_schema), account_data); + accumulator_input.persist(ai)?; + } + + msg!( + "[emit-updates]: initialized: {:?}, updated: {:?}", + initialized, + updated + ); + Ok(()) +} + +pub fn is_uninitialized_account(ai: &AccountInfo) -> bool { + ai.data_is_empty() && ai.owner == &system_program::ID +} + +#[derive(Accounts)] +pub struct PutAll<'info> { + #[account(mut)] + pub payer: Signer<'info>, + pub whitelist_verifier: WhitelistVerifier<'info>, + pub system_program: Program<'info, System>, + // remaining_accounts: - [AccumulatorInput PDAs] +} + +impl<'info> PutAll<'info> { + fn create_account<'a>( + account_info: &AccountInfo<'a>, + space: usize, + payer: &AccountInfo<'a>, + seeds: &[&[u8]], + rent: &Rent, + system_program: &AccountInfo<'a>, + ) -> Result<()> { + let lamports = rent.minimum_balance(space); + + system_program::create_account( + CpiContext::new_with_signer( + system_program.to_account_info(), + CreateAccount { + from: payer.to_account_info(), + to: account_info.to_account_info(), + }, + &[seeds], + ), + lamports, + space.try_into().unwrap(), + &crate::ID, + )?; + Ok(()) + } +} diff --git a/accumulator_updater/programs/accumulator_updater/src/lib.rs b/accumulator_updater/programs/accumulator_updater/src/lib.rs index 4169ebb7..27adbd3e 100644 --- a/accumulator_updater/programs/accumulator_updater/src/lib.rs +++ b/accumulator_updater/programs/accumulator_updater/src/lib.rs @@ -1,15 +1,12 @@ +mod instructions; mod macros; +mod state; -use anchor_lang::{ - prelude::*, - solana_program::sysvar::{ - self, - instructions::get_instruction_relative, - }, - system_program::{ - self, - CreateAccount, - }, + +use { + anchor_lang::prelude::*, + instructions::*, + state::*, }; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); @@ -18,6 +15,9 @@ declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); pub mod accumulator_updater { use super::*; + + /// Initializes the whitelist and sets it's authority to the provided pubkey + /// Once initialized, the authority must sign all further changes to the whitelist. pub fn initialize(ctx: Context, authority: Pubkey) -> Result<()> { require_keys_neq!(authority, Pubkey::default()); let whitelist = &mut ctx.accounts.whitelist; @@ -26,6 +26,10 @@ pub mod accumulator_updater { Ok(()) } + /// Sets the programs that are allowed to invoke this program through CPI + /// + /// * `allowed_programs` - Entire list of programs that are allowed to + /// invoke this program through CPI pub fn set_allowed_programs( ctx: Context, allowed_programs: Vec, @@ -36,6 +40,7 @@ pub mod accumulator_updater { Ok(()) } + /// Sets the new authority for the whitelist pub fn update_whitelist_authority( ctx: Context, new_authority: Pubkey, @@ -46,125 +51,25 @@ pub mod accumulator_updater { Ok(()) } - /// Add new account(s) to be included in the accumulator + + /// Create or update inputs for the Accumulator. Each input is written + /// into a separate PDA account derived with + /// seeds = [cpi_caller, b"accumulator", base_account_key, schema] /// - /// * `base_account` - Pubkey of the original account the - /// AccumulatorInput(s) are derived from - /// * `data` - Vec of AccumulatorInput account data - /// * `account_type` - Marker to indicate base_account account_type - /// * `account_schemas` - Vec of markers to indicate schemas for - /// AccumulatorInputs. In same respective - /// order as data - pub fn create_inputs<'info>( - ctx: Context<'_, '_, '_, 'info, CreateInputs<'info>>, - base_account: Pubkey, - data: Vec>, - account_type: u32, - account_schemas: Vec, + /// The caller of this instruction must pass those PDAs + /// while calling this function in the same order as elements. + /// + /// + /// * `base_account_key` - Pubkey of the original account the + /// AccumulatorInput(s) are derived from + /// * `values` - Vec of (schema, account_data) in same respective + /// order `ctx.remaining_accounts` + pub fn put_all<'info>( + ctx: Context<'_, '_, '_, 'info, PutAll<'info>>, + base_account_key: Pubkey, + values: Vec, ) -> Result<()> { - let cpi_caller = ctx.accounts.whitelist_verifier.is_allowed()?; - let accts = ctx.remaining_accounts; - require_eq!(accts.len(), data.len()); - require_eq!(data.len(), account_schemas.len()); - let mut zip = data.into_iter().zip(account_schemas.into_iter()); - - let rent = Rent::get()?; - - for ai in accts { - let (account_data, account_schema) = zip.next().unwrap(); - let seeds = accumulator_acc_seeds!(cpi_caller, base_account, account_schema); - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - require_keys_eq!(ai.key(), pda); - - //TODO: Update this with serialization logic - let accumulator_size = 8 + AccumulatorInput::get_initial_size(&account_data); - let accumulator_input = AccumulatorInput::new( - AccumulatorHeader::new( - 1, //from CPI caller? - account_type, - account_schema, - ), - account_data, - ); - CreateInputs::create_and_initialize_accumulator_input_pda( - ai, - accumulator_input, - accumulator_size, - &ctx.accounts.payer, - &[accumulator_acc_seeds_with_bump!( - cpi_caller, - base_account, - account_schema, - bump - )], - &rent, - &ctx.accounts.system_program, - )?; - } - - Ok(()) - } -} - - -// Note: purposely not making this zero_copy -// otherwise whitelist must always be marked mutable -// and majority of operations are read -#[account] -#[derive(InitSpace)] -pub struct Whitelist { - pub bump: u8, - pub authority: Pubkey, - #[max_len(32)] - pub allowed_programs: Vec, -} - -impl Whitelist { - pub fn validate_programs(&self, allowed_programs: &[Pubkey]) -> Result<()> { - require!( - !self.allowed_programs.contains(&Pubkey::default()), - AccumulatorUpdaterError::InvalidAllowedProgram - ); - require_gte!( - 32, - allowed_programs.len(), - AccumulatorUpdaterError::MaximumAllowedProgramsExceeded - ); - Ok(()) - } - - pub fn validate_new_authority(&self, new_authority: Pubkey) -> Result<()> { - require_keys_neq!(new_authority, Pubkey::default()); - Ok(()) - } -} - - -#[derive(Accounts)] -pub struct WhitelistVerifier<'info> { - #[account( - seeds = [b"accumulator".as_ref(), b"whitelist".as_ref()], - bump = whitelist.bump, - )] - pub whitelist: Account<'info, Whitelist>, - /// CHECK: Instruction introspection sysvar - #[account(address = sysvar::instructions::ID)] - pub ixs_sysvar: UncheckedAccount<'info>, -} - -impl<'info> WhitelistVerifier<'info> { - pub fn get_cpi_caller(&self) -> Result { - let instruction = get_instruction_relative(0, &self.ixs_sysvar.to_account_info())?; - Ok(instruction.program_id) - } - pub fn is_allowed(&self) -> Result { - let cpi_caller = self.get_cpi_caller()?; - let whitelist = &self.whitelist; - require!( - whitelist.allowed_programs.contains(&cpi_caller), - AccumulatorUpdaterError::CallerNotAllowed - ); - Ok(cpi_caller) + instructions::put_all(ctx, base_account_key, values) } } @@ -201,123 +106,6 @@ pub struct UpdateWhitelist<'info> { } -#[derive(Accounts)] -#[instruction(base_account: Pubkey, data: Vec>, account_type: u32)] // only needed if using optional accounts -pub struct CreateInputs<'info> { - #[account(mut)] - pub payer: Signer<'info>, - pub whitelist_verifier: WhitelistVerifier<'info>, - pub system_program: Program<'info, System>, - //TODO: decide on using optional accounts vs ctx.remaining_accounts - // - optional accounts can leverage anchor macros for PDA init/verification - // - ctx.remaining_accounts can be used to pass in any number of accounts - // - // https://github.com/coral-xyz/anchor/pull/2101 - anchor optional accounts PR - // #[account( - // init, - // payer = payer, - // seeds = [ - // whitelist_verifier.get_cpi_caller()?.as_ref(), - // b"accumulator".as_ref(), - // base_account.as_ref() - // &account_type.to_le_bytes(), - // ], - // bump, - // space = 8 + AccumulatorAccount::get_initial_size(&data[0]) - // )] - // pub acc_input_0: Option>, -} - -impl<'info> CreateInputs<'info> { - fn create_and_initialize_accumulator_input_pda<'a>( - accumulator_input_ai: &AccountInfo<'a>, - accumulator_input: AccumulatorInput, - accumulator_input_size: usize, - payer: &AccountInfo<'a>, - seeds: &[&[&[u8]]], - rent: &Rent, - system_program: &AccountInfo<'a>, - ) -> Result<()> { - let lamports = rent.minimum_balance(accumulator_input_size); - - system_program::create_account( - CpiContext::new_with_signer( - system_program.to_account_info(), - CreateAccount { - from: payer.to_account_info(), - to: accumulator_input_ai.to_account_info(), - }, - seeds, - ), - lamports, - accumulator_input_size.try_into().unwrap(), - &crate::ID, - )?; - - AccountSerialize::try_serialize( - &accumulator_input, - &mut &mut accumulator_input_ai.data.borrow_mut()[..], - ) - .map_err(|e| { - msg!("original error: {:?}", e); - AccumulatorUpdaterError::SerializeError - })?; - // msg!("accumulator_input_ai: {:#?}", accumulator_input_ai); - - Ok(()) - } -} - -// TODO: should UpdateInput be allowed to resize an AccumulatorInput account? -#[derive(Accounts)] -pub struct UpdateInputs<'info> { - #[account(mut)] - pub payer: Signer<'info>, - pub whitelist_verifier: WhitelistVerifier<'info>, -} - -//TODO: implement custom serialization & set alignment -#[account] -pub struct AccumulatorInput { - pub header: AccumulatorHeader, - //TODO: Vec for resizing? - pub data: Vec, -} - -impl AccumulatorInput { - pub fn get_initial_size(data: &Vec) -> usize { - AccumulatorHeader::SIZE + 4 + data.len() - } - - pub fn new(header: AccumulatorHeader, data: Vec) -> Self { - Self { header, data } - } -} - -//TODO: -// - implement custom serialization & set alignment -// - what other fields are needed? -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Default)] -pub struct AccumulatorHeader { - pub version: u8, - // u32 for parity with pyth oracle contract - pub account_type: u32, - pub account_schema: u8, -} - - -impl AccumulatorHeader { - pub const SIZE: usize = 1 + 4 + 1; - - pub fn new(version: u8, account_type: u32, account_schema: u8) -> Self { - Self { - version, - account_type, - account_schema, - } - } -} - #[error_code] pub enum AccumulatorUpdaterError { #[msg("CPI Caller not allowed")] @@ -334,4 +122,10 @@ pub enum AccumulatorUpdaterError { InvalidAllowedProgram, #[msg("Maximum number of allowed programs exceeded")] MaximumAllowedProgramsExceeded, + #[msg("Invalid PDA")] + InvalidPDA, + #[msg("Update data exceeds current length")] + CurrentDataLengthExceeded, + #[msg("Accumulator Input not writable")] + AccumulatorInputNotWritable, } diff --git a/accumulator_updater/programs/accumulator_updater/src/macros.rs b/accumulator_updater/programs/accumulator_updater/src/macros.rs index bdfc0134..97d3140b 100644 --- a/accumulator_updater/programs/accumulator_updater/src/macros.rs +++ b/accumulator_updater/programs/accumulator_updater/src/macros.rs @@ -1,25 +1,12 @@ #[macro_export] -macro_rules! accumulator_acc_seeds { - ($cpi_caller_pid:expr, $base_account:expr, $account_type:expr) => { +macro_rules! accumulator_input_seeds { + ($accumulator_input:expr, $cpi_caller_pid:expr, $base_account:expr) => { &[ $cpi_caller_pid.as_ref(), b"accumulator".as_ref(), $base_account.as_ref(), - &$account_type.to_le_bytes(), - ] - }; -} - - -#[macro_export] -macro_rules! accumulator_acc_seeds_with_bump { - ($cpi_caller_pid:expr, $base_account:expr, $account_type:expr, $bump:expr) => { - &[ - $cpi_caller_pid.as_ref(), - b"accumulator".as_ref(), - $base_account.as_ref(), - &$account_type.to_le_bytes(), - &[$bump], + &$accumulator_input.header.account_schema.to_le_bytes(), + &[$accumulator_input.header.bump], ] }; } diff --git a/accumulator_updater/programs/accumulator_updater/src/state/accumulator_input.rs b/accumulator_updater/programs/accumulator_updater/src/state/accumulator_input.rs new file mode 100644 index 00000000..757f564d --- /dev/null +++ b/accumulator_updater/programs/accumulator_updater/src/state/accumulator_input.rs @@ -0,0 +1,90 @@ +use { + crate::{ + accumulator_input_seeds, + AccumulatorUpdaterError, + }, + anchor_lang::prelude::*, +}; + +/// `AccumulatorInput` is an arbitrary set of bytes +/// that will be included in the AccumulatorSysvar +/// +/// +/// The actual contents of data are set/handled by +/// the CPI calling program (e.g. Pyth Oracle) +/// +/// TODO: implement custom serialization & set alignment +#[account] +pub struct AccumulatorInput { + pub header: AccumulatorHeader, + pub data: Vec, +} + +impl AccumulatorInput { + pub fn size(data: &Vec) -> usize { + AccumulatorHeader::SIZE + 4 + data.len() + } + + pub fn new(header: AccumulatorHeader, data: Vec) -> Self { + Self { header, data } + } + + pub fn update(&mut self, new_data: Vec) { + self.header = AccumulatorHeader::new(self.header.bump, self.header.account_schema); + self.data = new_data; + } + + fn derive_pda(&self, cpi_caller: Pubkey, base_account: Pubkey) -> Result { + let res = Pubkey::create_program_address( + accumulator_input_seeds!(self, cpi_caller, base_account), + &crate::ID, + ) + .map_err(|_| AccumulatorUpdaterError::InvalidPDA)?; + Ok(res) + } + + pub fn validate( + &self, + key: Pubkey, + cpi_caller: Pubkey, + base_account: Pubkey, + account_schema: u8, + ) -> Result<()> { + let expected_key = self.derive_pda(cpi_caller, base_account)?; + require_keys_eq!(expected_key, key); + require_eq!(self.header.account_schema, account_schema); + Ok(()) + } + + pub fn persist(&self, ai: &AccountInfo) -> Result<()> { + AccountSerialize::try_serialize(self, &mut &mut ai.data.borrow_mut()[..]).map_err(|e| { + msg!("original error: {:?}", e); + AccumulatorUpdaterError::SerializeError + })?; + Ok(()) + } +} + +//TODO: +// - implement custom serialization & set alignment +// - what other fields are needed? +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Default)] +pub struct AccumulatorHeader { + pub bump: u8, + pub version: u8, + pub account_schema: u8, +} + + +impl AccumulatorHeader { + pub const SIZE: usize = 1 + 1 + 1; + pub const CURRENT_VERSION: u8 = 1; + + pub fn new(bump: u8, account_schema: u8) -> Self { + Self { + bump, + version: Self::CURRENT_VERSION, + account_schema, + } + } +} diff --git a/accumulator_updater/programs/accumulator_updater/src/state/mod.rs b/accumulator_updater/programs/accumulator_updater/src/state/mod.rs new file mode 100644 index 00000000..46cdbaca --- /dev/null +++ b/accumulator_updater/programs/accumulator_updater/src/state/mod.rs @@ -0,0 +1,7 @@ +pub use { + accumulator_input::*, + whitelist::*, +}; + +mod accumulator_input; +mod whitelist; diff --git a/accumulator_updater/programs/accumulator_updater/src/state/whitelist.rs b/accumulator_updater/programs/accumulator_updater/src/state/whitelist.rs new file mode 100644 index 00000000..8986f4e0 --- /dev/null +++ b/accumulator_updater/programs/accumulator_updater/src/state/whitelist.rs @@ -0,0 +1,70 @@ +use { + crate::AccumulatorUpdaterError, + anchor_lang::{ + prelude::*, + solana_program::sysvar::{ + self, + instructions::get_instruction_relative, + }, + }, +}; + +// Note: purposely not making this zero_copy +// otherwise whitelist must always be marked mutable +// and majority of operations are read +#[account] +#[derive(InitSpace)] +pub struct Whitelist { + pub bump: u8, + pub authority: Pubkey, + #[max_len(32)] + pub allowed_programs: Vec, +} + +impl Whitelist { + pub fn validate_programs(&self, allowed_programs: &[Pubkey]) -> Result<()> { + require!( + !self.allowed_programs.contains(&Pubkey::default()), + AccumulatorUpdaterError::InvalidAllowedProgram + ); + require_gte!( + 32, + allowed_programs.len(), + AccumulatorUpdaterError::MaximumAllowedProgramsExceeded + ); + Ok(()) + } + + pub fn validate_new_authority(&self, new_authority: Pubkey) -> Result<()> { + require_keys_neq!(new_authority, Pubkey::default()); + Ok(()) + } +} + +#[derive(Accounts)] +pub struct WhitelistVerifier<'info> { + #[account( + seeds = [b"accumulator".as_ref(), b"whitelist".as_ref()], + bump = whitelist.bump, + )] + pub whitelist: Account<'info, Whitelist>, + /// CHECK: Instruction introspection sysvar + #[account(address = sysvar::instructions::ID)] + pub ixs_sysvar: UncheckedAccount<'info>, +} + +impl<'info> WhitelistVerifier<'info> { + pub fn get_cpi_caller(&self) -> Result { + let instruction = get_instruction_relative(0, &self.ixs_sysvar.to_account_info())?; + Ok(instruction.program_id) + } + pub fn is_allowed(&self) -> Result { + let cpi_caller = self.get_cpi_caller()?; + let whitelist = &self.whitelist; + require!( + whitelist.allowed_programs.contains(&cpi_caller), + AccumulatorUpdaterError::CallerNotAllowed + ); + Ok(cpi_caller) + } +} diff --git a/accumulator_updater/programs/mock-cpi-caller/src/instructions/add_price.rs b/accumulator_updater/programs/mock-cpi-caller/src/instructions/add_price.rs new file mode 100644 index 00000000..b55e1dfd --- /dev/null +++ b/accumulator_updater/programs/mock-cpi-caller/src/instructions/add_price.rs @@ -0,0 +1,134 @@ +use { + crate::{ + instructions::{ + sighash, + ACCUMULATOR_UPDATER_IX_NAME, + }, + message::{ + get_schemas, + price::{ + CompactPriceMessage, + FullPriceMessage, + }, + AccumulatorSerializer, + }, + state::{ + PriceAccount, + PythAccountType, + }, + }, + accumulator_updater::program::AccumulatorUpdater as AccumulatorUpdaterProgram, + anchor_lang::{ + prelude::*, + solana_program::sysvar, + }, +}; + +pub fn add_price<'info>( + ctx: Context<'_, '_, '_, 'info, AddPrice<'info>>, + params: AddPriceParams, +) -> Result<()> { + let mut account_data: Vec> = vec![]; + let schemas = get_schemas(PythAccountType::Price); + + { + let pyth_price_acct = &mut ctx.accounts.pyth_price_account.load_init()?; + + pyth_price_acct.init(params)?; + + let price_full_data = FullPriceMessage::from(&**pyth_price_acct).accumulator_serialize()?; + + account_data.push(price_full_data); + + + let price_compact_data = + CompactPriceMessage::from(&**pyth_price_acct).accumulator_serialize()?; + account_data.push(price_compact_data); + } + + + let values = schemas + .into_iter() + .map(|s| s.to_u8()) + .zip(account_data) + .collect::)>>(); + + // Note: normally pyth oracle add_price wouldn't call emit_accumulator_inputs + // since add_price doesn't actually add/update any price data we would + // want included in the accumulator anyways. This is just for testing + AddPrice::emit_accumulator_inputs(ctx, values) +} + + +impl<'info> AddPrice<'info> { + /// Invoke accumulator-updater emit-inputs ix cpi call using solana + pub fn emit_accumulator_inputs( + ctx: Context<'_, '_, '_, 'info, AddPrice<'info>>, + values: Vec<(u8, Vec)>, + ) -> anchor_lang::Result<()> { + let mut accounts = vec![ + AccountMeta::new(ctx.accounts.payer.key(), true), + AccountMeta::new_readonly(ctx.accounts.accumulator_whitelist.key(), false), + AccountMeta::new_readonly(ctx.accounts.ixs_sysvar.key(), false), + AccountMeta::new_readonly(ctx.accounts.system_program.key(), false), + ]; + accounts.extend_from_slice( + &ctx.remaining_accounts + .iter() + .map(|a| AccountMeta::new(a.key(), false)) + .collect::>(), + ); + let create_inputs_ix = anchor_lang::solana_program::instruction::Instruction { + program_id: ctx.accounts.accumulator_program.key(), + accounts, + data: ( + //anchor ix discriminator/identifier + sighash("global", ACCUMULATOR_UPDATER_IX_NAME), + ctx.accounts.pyth_price_account.key(), + values, + // account_data, + // account_schemas, + ) + .try_to_vec() + .unwrap(), + }; + let account_infos = &mut ctx.accounts.to_account_infos(); + account_infos.extend_from_slice(ctx.remaining_accounts); + anchor_lang::solana_program::program::invoke(&create_inputs_ix, account_infos)?; + Ok(()) + } +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)] +pub struct AddPriceParams { + pub id: u64, + pub price: u64, + pub price_expo: u64, + pub ema: u64, + pub ema_expo: u64, +} + +#[derive(Accounts)] +#[instruction(params: AddPriceParams)] +pub struct AddPrice<'info> { + #[account( + init, + payer = payer, + seeds = [b"pyth".as_ref(), b"price".as_ref(), ¶ms.id.to_le_bytes()], + bump, + space = 8 + PriceAccount::INIT_SPACE + )] + pub pyth_price_account: AccountLoader<'info, PriceAccount>, + #[account(mut)] + pub payer: Signer<'info>, + /// also needed for accumulator_updater + pub system_program: Program<'info, System>, + /// CHECK: whitelist + pub accumulator_whitelist: UncheckedAccount<'info>, + /// CHECK: instructions introspection sysvar + #[account(address = sysvar::instructions::ID)] + pub ixs_sysvar: UncheckedAccount<'info>, + pub accumulator_program: Program<'info, AccumulatorUpdaterProgram>, + // Remaining Accounts + // should all be new uninitialized accounts +} diff --git a/accumulator_updater/programs/mock-cpi-caller/src/instructions/mod.rs b/accumulator_updater/programs/mock-cpi-caller/src/instructions/mod.rs new file mode 100644 index 00000000..f495719e --- /dev/null +++ b/accumulator_updater/programs/mock-cpi-caller/src/instructions/mod.rs @@ -0,0 +1,23 @@ +use anchor_lang::solana_program::hash::hashv; +pub use { + add_price::*, + update_price::*, +}; +mod add_price; +mod update_price; + +/// Generate discriminator to be able to call anchor program's ix +/// * `namespace` - "global" for instructions +/// * `name` - name of ix to call CASE-SENSITIVE +/// +/// Note: this could probably be converted into a constant hash value +/// since it will always be the same. +pub fn sighash(namespace: &str, name: &str) -> [u8; 8] { + let preimage = format!("{namespace}:{name}"); + + let mut sighash = [0u8; 8]; + sighash.copy_from_slice(&hashv(&[preimage.as_bytes()]).to_bytes()[..8]); + sighash +} + +pub const ACCUMULATOR_UPDATER_IX_NAME: &str = "put_all"; diff --git a/accumulator_updater/programs/mock-cpi-caller/src/instructions/update_price.rs b/accumulator_updater/programs/mock-cpi-caller/src/instructions/update_price.rs new file mode 100644 index 00000000..e66f965e --- /dev/null +++ b/accumulator_updater/programs/mock-cpi-caller/src/instructions/update_price.rs @@ -0,0 +1,134 @@ +use { + crate::{ + instructions::{ + sighash, + ACCUMULATOR_UPDATER_IX_NAME, + }, + message::{ + get_schemas, + price::{ + CompactPriceMessage, + FullPriceMessage, + }, + AccumulatorSerializer, + }, + state::{ + PriceAccount, + PythAccountType, + }, + }, + accumulator_updater::program::AccumulatorUpdater as AccumulatorUpdaterProgram, + anchor_lang::{ + prelude::*, + solana_program::sysvar, + }, +}; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)] +pub struct UpdatePriceParams { + pub price: u64, + pub price_expo: u64, + pub ema: u64, + pub ema_expo: u64, +} + + +#[derive(Accounts)] +pub struct UpdatePrice<'info> { + #[account( + mut, + seeds = [ + b"pyth".as_ref(), + b"price".as_ref(), + &pyth_price_account.load()?.id.to_le_bytes() + ], + bump, + )] + pub pyth_price_account: AccountLoader<'info, PriceAccount>, + #[account(mut)] + pub payer: Signer<'info>, + /// Needed for accumulator_updater + pub system_program: Program<'info, System>, + /// CHECK: whitelist + pub accumulator_whitelist: UncheckedAccount<'info>, + /// CHECK: instructions introspection sysvar + #[account(address = sysvar::instructions::ID)] + pub ixs_sysvar: UncheckedAccount<'info>, + pub accumulator_program: Program<'info, AccumulatorUpdaterProgram>, +} + +/// Updates the mock pyth price account and calls accumulator-updater +/// update_inputs ix +pub fn update_price<'info>( + ctx: Context<'_, '_, '_, 'info, UpdatePrice<'info>>, + params: UpdatePriceParams, +) -> Result<()> { + let mut account_data = vec![]; + let schemas = get_schemas(PythAccountType::Price); + + { + let pyth_price_acct = &mut ctx.accounts.pyth_price_account.load_mut()?; + pyth_price_acct.update(params)?; + + let price_full_data = FullPriceMessage::from(&**pyth_price_acct).accumulator_serialize()?; + + account_data.push(price_full_data); + + + let price_compact_data = + CompactPriceMessage::from(&**pyth_price_acct).accumulator_serialize()?; + account_data.push(price_compact_data); + } + + + // let account_schemas = schemas.into_iter().map(|s| s.to_u8()).collect::>(); + + let values = schemas + .into_iter() + .map(|s| s.to_u8()) + .zip(account_data) + .collect::)>>(); + + UpdatePrice::emit_accumulator_inputs(ctx, values) +} + +impl<'info> UpdatePrice<'info> { + /// Invoke accumulator-updater emit-inputs ix cpi call + pub fn emit_accumulator_inputs( + ctx: Context<'_, '_, '_, 'info, UpdatePrice<'info>>, + values: Vec<(u8, Vec)>, + // account_data: Vec>, + // account_schemas: Vec, + ) -> anchor_lang::Result<()> { + let mut accounts = vec![ + AccountMeta::new(ctx.accounts.payer.key(), true), + AccountMeta::new_readonly(ctx.accounts.accumulator_whitelist.key(), false), + AccountMeta::new_readonly(ctx.accounts.ixs_sysvar.key(), false), + AccountMeta::new_readonly(ctx.accounts.system_program.key(), false), + ]; + accounts.extend_from_slice( + &ctx.remaining_accounts + .iter() + .map(|a| AccountMeta::new(a.key(), false)) + .collect::>(), + ); + let update_inputs_ix = anchor_lang::solana_program::instruction::Instruction { + program_id: ctx.accounts.accumulator_program.key(), + accounts, + data: ( + //anchor ix discriminator/identifier + sighash("global", ACCUMULATOR_UPDATER_IX_NAME), + ctx.accounts.pyth_price_account.key(), + values, + // account_data, + // account_schemas, + ) + .try_to_vec() + .unwrap(), + }; + let account_infos = &mut ctx.accounts.to_account_infos(); + account_infos.extend_from_slice(ctx.remaining_accounts); + anchor_lang::solana_program::program::invoke(&update_inputs_ix, account_infos)?; + Ok(()) + } +} diff --git a/accumulator_updater/programs/mock-cpi-caller/src/lib.rs b/accumulator_updater/programs/mock-cpi-caller/src/lib.rs index 89738c3a..0615e74d 100644 --- a/accumulator_updater/programs/mock-cpi-caller/src/lib.rs +++ b/accumulator_updater/programs/mock-cpi-caller/src/lib.rs @@ -1,236 +1,33 @@ use { - accumulator_updater::{ - cpi::accounts as AccumulatorUpdaterCpiAccts, - program::AccumulatorUpdater as AccumulatorUpdaterProgram, - }, - anchor_lang::{ - prelude::*, - solana_program::{ - hash::hashv, - sysvar, - }, - }, - message::{ - get_schemas, - price::*, - AccumulatorSerializer, - }, + anchor_lang::prelude::*, + instructions::*, }; +pub mod instructions; pub mod message; +mod state; + declare_id!("Dg5PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); #[program] pub mod mock_cpi_caller { use super::*; + /// Creates a `PriceAccount` with the given parameters pub fn add_price<'info>( ctx: Context<'_, '_, '_, 'info, AddPrice<'info>>, params: AddPriceParams, ) -> Result<()> { - let mut account_data: Vec> = vec![]; - let schemas = get_schemas(PythAccountType::Price); - - { - let pyth_price_acct = &mut ctx.accounts.pyth_price_account.load_init()?; - - pyth_price_acct.init(params)?; - - let price_full_data = - FullPriceMessage::from(&**pyth_price_acct).accumulator_serialize()?; - - account_data.push(price_full_data); - - - let price_compact_data = - CompactPriceMessage::from(&**pyth_price_acct).accumulator_serialize()?; - account_data.push(price_compact_data); - } - - - let account_schemas = schemas.into_iter().map(|s| s.to_u8()).collect::>(); - - // 44444 compute units - // AddPrice::invoke_cpi_anchor(ctx, account_data, PythAccountType::Price, account_schemas) - // 44045 compute units - AddPrice::invoke_cpi_solana(ctx, account_data, PythAccountType::Price, account_schemas) - } -} - - -impl<'info> AddPrice<'info> { - fn create_inputs_ctx( - &self, - remaining_accounts: &[AccountInfo<'info>], - ) -> CpiContext<'_, '_, '_, 'info, AccumulatorUpdaterCpiAccts::CreateInputs<'info>> { - let mut cpi_ctx = CpiContext::new( - self.accumulator_program.to_account_info(), - AccumulatorUpdaterCpiAccts::CreateInputs { - payer: self.payer.to_account_info(), - whitelist_verifier: AccumulatorUpdaterCpiAccts::WhitelistVerifier { - whitelist: self.accumulator_whitelist.to_account_info(), - ixs_sysvar: self.ixs_sysvar.to_account_info(), - }, - system_program: self.system_program.to_account_info(), - }, - ); - - - cpi_ctx = cpi_ctx.with_remaining_accounts(remaining_accounts.to_vec()); - cpi_ctx + instructions::add_price(ctx, params) } - /// invoke cpi call using anchor - fn invoke_cpi_anchor( - ctx: Context<'_, '_, '_, 'info, AddPrice<'info>>, - account_data: Vec>, - account_type: PythAccountType, - account_schemas: Vec, + /// Updates a `PriceAccount` with the given parameters + pub fn update_price<'info>( + ctx: Context<'_, '_, '_, 'info, UpdatePrice<'info>>, + params: UpdatePriceParams, ) -> Result<()> { - accumulator_updater::cpi::create_inputs( - ctx.accounts.create_inputs_ctx(ctx.remaining_accounts), - ctx.accounts.pyth_price_account.key(), - account_data, - account_type.to_u32(), - account_schemas, - )?; - Ok(()) + instructions::update_price(ctx, params) } - - - /// invoke cpi call using solana - fn invoke_cpi_solana( - ctx: Context<'_, '_, '_, 'info, AddPrice<'info>>, - account_data: Vec>, - account_type: PythAccountType, - account_schemas: Vec, - ) -> Result<()> { - let mut accounts = vec![ - AccountMeta::new(ctx.accounts.payer.key(), true), - AccountMeta::new_readonly(ctx.accounts.accumulator_whitelist.key(), false), - AccountMeta::new_readonly(ctx.accounts.ixs_sysvar.key(), false), - AccountMeta::new_readonly(ctx.accounts.system_program.key(), false), - ]; - accounts.extend_from_slice( - &ctx.remaining_accounts - .iter() - .map(|a| AccountMeta::new(a.key(), false)) - .collect::>(), - ); - let create_inputs_ix = anchor_lang::solana_program::instruction::Instruction { - program_id: ctx.accounts.accumulator_program.key(), - accounts, - data: ( - //anchor ix discriminator/identifier - sighash("global", "create_inputs"), - ctx.accounts.pyth_price_account.key(), - account_data, - account_type.to_u32(), - account_schemas, - ) - .try_to_vec() - .unwrap(), - }; - let account_infos = &mut ctx.accounts.to_account_infos(); - account_infos.extend_from_slice(ctx.remaining_accounts); - anchor_lang::solana_program::program::invoke(&create_inputs_ix, account_infos)?; - Ok(()) - } -} - - -/// Generate discriminator to be able to call anchor program's ix -/// * `namespace` - "global" for instructions -/// * `name` - name of ix to call CASE-SENSITIVE -pub fn sighash(namespace: &str, name: &str) -> [u8; 8] { - let preimage = format!("{namespace}:{name}"); - - let mut sighash = [0u8; 8]; - sighash.copy_from_slice(&hashv(&[preimage.as_bytes()]).to_bytes()[..8]); - sighash -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)] -pub struct AddPriceParams { - pub id: u64, - pub price: u64, - pub price_expo: u64, - pub ema: u64, - pub ema_expo: u64, -} - -trait PythAccount { - const ACCOUNT_TYPE: PythAccountType; - fn account_type() -> PythAccountType { - Self::ACCOUNT_TYPE - } -} - -#[derive(Copy, Clone)] -#[repr(u32)] -pub enum PythAccountType { - Mapping = 1, - Product = 2, - Price = 3, - Test = 4, - Permissions = 5, -} -impl PythAccountType { - fn to_u32(&self) -> u32 { - *self as u32 - } -} - -#[derive(Accounts)] -#[instruction(params: AddPriceParams)] -pub struct AddPrice<'info> { - #[account( - init, - payer = payer, - seeds = [b"pyth".as_ref(), b"price".as_ref(), ¶ms.id.to_le_bytes()], - bump, - space = 8 + PriceAccount::INIT_SPACE - )] - pub pyth_price_account: AccountLoader<'info, PriceAccount>, - #[account(mut)] - pub payer: Signer<'info>, - /// also needed for accumulator_updater - pub system_program: Program<'info, System>, - /// CHECK: whitelist - pub accumulator_whitelist: UncheckedAccount<'info>, - /// CHECK: instructions introspection sysvar - #[account(address = sysvar::instructions::ID)] - pub ixs_sysvar: UncheckedAccount<'info>, - pub accumulator_program: Program<'info, AccumulatorUpdaterProgram>, - // Remaining Accounts - // should all be new uninitialized accounts -} - - -#[account(zero_copy)] -#[derive(InitSpace)] -pub struct PriceAccount { - pub id: u64, - pub price: u64, - pub price_expo: u64, - pub ema: u64, - pub ema_expo: u64, - pub comp_: [Pubkey; 32], -} - -impl PriceAccount { - fn init(&mut self, params: AddPriceParams) -> Result<()> { - self.id = params.id; - self.price = params.price; - self.price_expo = params.price_expo; - self.ema = params.ema; - self.ema_expo = params.ema_expo; - Ok(()) - } -} - -impl PythAccount for PriceAccount { - const ACCOUNT_TYPE: PythAccountType = PythAccountType::Price; } @@ -243,15 +40,13 @@ mod test { #[test] fn ix_discriminator() { - let a = &(accumulator_updater::instruction::CreateInputs { - base_account: anchor_lang::prelude::Pubkey::default(), - data: vec![], - account_type: 0, - account_schemas: vec![], + let a = &(accumulator_updater::instruction::PutAll { + base_account_key: anchor_lang::prelude::Pubkey::default(), + values: vec![], } .data()[..8]); - let sighash = sighash("global", "create_inputs"); + let sighash = sighash("global", ACCUMULATOR_UPDATER_IX_NAME); println!( r" a: {a:?} diff --git a/accumulator_updater/programs/mock-cpi-caller/src/message.rs b/accumulator_updater/programs/mock-cpi-caller/src/message.rs index a3c982a5..bee29899 100644 --- a/accumulator_updater/programs/mock-cpi-caller/src/message.rs +++ b/accumulator_updater/programs/mock-cpi-caller/src/message.rs @@ -1,4 +1,4 @@ -use crate::PythAccountType; +use crate::state::PythAccountType; pub mod price; diff --git a/accumulator_updater/programs/mock-cpi-caller/src/message/price.rs b/accumulator_updater/programs/mock-cpi-caller/src/message/price.rs index 7a0ac025..66fd012b 100644 --- a/accumulator_updater/programs/mock-cpi-caller/src/message/price.rs +++ b/accumulator_updater/programs/mock-cpi-caller/src/message/price.rs @@ -1,7 +1,7 @@ use { crate::{ message::AccumulatorSerializer, - PriceAccount, + state::PriceAccount, }, anchor_lang::prelude::*, bytemuck::{ diff --git a/accumulator_updater/programs/mock-cpi-caller/src/state/mod.rs b/accumulator_updater/programs/mock-cpi-caller/src/state/mod.rs new file mode 100644 index 00000000..83485111 --- /dev/null +++ b/accumulator_updater/programs/mock-cpi-caller/src/state/mod.rs @@ -0,0 +1,25 @@ +pub use price::*; + +mod price; + +trait PythAccount { + const ACCOUNT_TYPE: PythAccountType; + fn account_type() -> PythAccountType { + Self::ACCOUNT_TYPE + } +} + +#[derive(Copy, Clone)] +#[repr(u32)] +pub enum PythAccountType { + Mapping = 1, + Product = 2, + Price = 3, + Test = 4, + Permissions = 5, +} +impl PythAccountType { + pub(crate) fn to_u32(&self) -> u32 { + *self as u32 + } +} diff --git a/accumulator_updater/programs/mock-cpi-caller/src/state/price.rs b/accumulator_updater/programs/mock-cpi-caller/src/state/price.rs new file mode 100644 index 00000000..fd666290 --- /dev/null +++ b/accumulator_updater/programs/mock-cpi-caller/src/state/price.rs @@ -0,0 +1,47 @@ +use { + crate::{ + instructions::{ + AddPriceParams, + UpdatePriceParams, + }, + state::{ + PythAccount, + PythAccountType, + }, + }, + anchor_lang::prelude::*, +}; + +#[account(zero_copy)] +#[derive(InitSpace)] +pub struct PriceAccount { + pub id: u64, + pub price: u64, + pub price_expo: u64, + pub ema: u64, + pub ema_expo: u64, + pub comp_: [Pubkey; 32], +} + +impl PriceAccount { + pub(crate) fn init(&mut self, params: AddPriceParams) -> Result<()> { + self.id = params.id; + self.price = params.price; + self.price_expo = params.price_expo; + self.ema = params.ema; + self.ema_expo = params.ema_expo; + Ok(()) + } + + pub(crate) fn update(&mut self, params: UpdatePriceParams) -> Result<()> { + self.price = params.price; + self.price_expo = params.price_expo; + self.ema = params.ema; + self.ema_expo = params.ema_expo; + Ok(()) + } +} + +impl PythAccount for PriceAccount { + const ACCOUNT_TYPE: PythAccountType = PythAccountType::Price; +} diff --git a/accumulator_updater/tests/accumulator_updater.ts b/accumulator_updater/tests/accumulator_updater.ts index 1e6ff1ab..f334682d 100644 --- a/accumulator_updater/tests/accumulator_updater.ts +++ b/accumulator_updater/tests/accumulator_updater.ts @@ -4,7 +4,7 @@ import { AccumulatorUpdater } from "../target/types/accumulator_updater"; import { MockCpiCaller } from "../target/types/mock_cpi_caller"; import lumina from "@lumina-dev/test"; import { assert } from "chai"; -import { ComputeBudgetProgram } from "@solana/web3.js"; +import { AccountMeta, ComputeBudgetProgram } from "@solana/web3.js"; import bs58 from "bs58"; // Enables tool that runs in local browser for easier debugging of @@ -16,6 +16,25 @@ const accumulatorUpdaterProgram = anchor.workspace const mockCpiProg = anchor.workspace.MockCpiCaller as Program; let whitelistAuthority = anchor.web3.Keypair.generate(); +const pythPriceAccountId = new anchor.BN(1); +const addPriceParams = { + id: pythPriceAccountId, + price: new anchor.BN(2), + priceExpo: new anchor.BN(3), + ema: new anchor.BN(4), + emaExpo: new anchor.BN(5), +}; +const [pythPriceAccountPk] = anchor.web3.PublicKey.findProgramAddressSync( + [ + Buffer.from("pyth"), + Buffer.from("price"), + pythPriceAccountId.toArrayLike(Buffer, "le", 8), + ], + mockCpiProg.programId +); + +const PRICE_SCHEMAS = [0, 1]; + describe("accumulator_updater", () => { // Configure the client to use the local cluster. let provider = anchor.AnchorProvider.env(); @@ -84,14 +103,6 @@ describe("accumulator_updater", () => { }); it("Mock CPI program - AddPrice", async () => { - const addPriceParams = { - id: new anchor.BN(1), - price: new anchor.BN(2), - priceExpo: new anchor.BN(3), - ema: new anchor.BN(4), - emaExpo: new anchor.BN(5), - }; - const mockCpiCallerAddPriceTxPubkeys = await mockCpiProg.methods .addPrice(addPriceParams) .accounts({ @@ -102,17 +113,19 @@ describe("accumulator_updater", () => { }) .pubkeys(); - const accumulatorPdas = [0, 1].map((pythSchema) => { - const [pda] = anchor.web3.PublicKey.findProgramAddressSync( + const accumulatorPdaKeys = PRICE_SCHEMAS.map((pythSchema) => { + return anchor.web3.PublicKey.findProgramAddressSync( [ mockCpiProg.programId.toBuffer(), Buffer.from("accumulator"), - mockCpiCallerAddPriceTxPubkeys.pythPriceAccount.toBuffer(), + // mockCpiCallerAddPriceTxPubkeys.pythPriceAccount.toBuffer(), + pythPriceAccountPk.toBuffer(), new anchor.BN(pythSchema).toArrayLike(Buffer, "le", 1), ], accumulatorUpdaterProgram.programId - ); - console.log(`pda for pyth schema ${pythSchema}: ${pda.toString()}`); + )[0]; + }); + const accumulatorPdaMetas = accumulatorPdaKeys.map((pda) => { return { pubkey: pda, isSigner: false, @@ -126,7 +139,7 @@ describe("accumulator_updater", () => { .accounts({ ...mockCpiCallerAddPriceTxPubkeys, }) - .remainingAccounts(accumulatorPdas) + .remainingAccounts(accumulatorPdaMetas) .prepare(); console.log( @@ -153,7 +166,7 @@ describe("accumulator_updater", () => { .accounts({ ...mockCpiCallerAddPriceTxPubkeys, }) - .remainingAccounts(accumulatorPdas) + .remainingAccounts(accumulatorPdaMetas) .preInstructions([ ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), ]) @@ -165,28 +178,31 @@ describe("accumulator_updater", () => { const pythPriceAccount = await provider.connection.getAccountInfo( mockCpiCallerAddPriceTxPubkeys.pythPriceAccount ); - console.log(`pythPriceAccount: ${pythPriceAccount.data.toString("hex")}`); - const accumulatorInputKeys = accumulatorPdas.map((a) => a.pubkey); + const pythPriceAcct = { + ...pythPriceAccount, + data: pythPriceAccount.data.toString("hex"), + }; + console.log(`pythPriceAccount: ${JSON.stringify(pythPriceAcct)}`); const accumulatorInputs = await accumulatorUpdaterProgram.account.accumulatorInput.fetchMultiple( - accumulatorInputKeys + accumulatorPdaKeys ); - const accumulatorPriceAccounts = accumulatorInputs.map((ai) => { + const accumulatorPriceMessages = accumulatorInputs.map((ai) => { return parseAccumulatorInput(ai); }); console.log( - `accumulatorPriceAccounts: ${JSON.stringify( - accumulatorPriceAccounts, + `accumulatorPriceMessages: ${JSON.stringify( + accumulatorPriceMessages, null, 2 )}` ); - accumulatorPriceAccounts.forEach((pa) => { - assert.isTrue(pa.id.eq(addPriceParams.id)); - assert.isTrue(pa.price.eq(addPriceParams.price)); - assert.isTrue(pa.priceExpo.eq(addPriceParams.priceExpo)); + accumulatorPriceMessages.forEach((pm) => { + assert.isTrue(pm.id.eq(addPriceParams.id)); + assert.isTrue(pm.price.eq(addPriceParams.price)); + assert.isTrue(pm.priceExpo.eq(addPriceParams.priceExpo)); }); let discriminator = @@ -207,15 +223,96 @@ describe("accumulator_updater", () => { ], } ); - const accumulatorInputKeyStrings = accumulatorInputKeys.map((k) => + const accumulatorInputKeyStrings = accumulatorPdaKeys.map((k) => k.toString() ); accumulatorAccounts.forEach((a) => { assert.isTrue(accumulatorInputKeyStrings.includes(a.pubkey.toString())); }); }); + + it("Mock CPI Program - UpdatePrice", async () => { + const updatePriceParams = { + price: new anchor.BN(5), + priceExpo: new anchor.BN(6), + ema: new anchor.BN(7), + emaExpo: new anchor.BN(8), + }; + + let accumulatorPdaMetas = getAccumulatorPdaMetas( + pythPriceAccountPk, + PRICE_SCHEMAS + ); + await mockCpiProg.methods + .updatePrice(updatePriceParams) + .accounts({ + pythPriceAccount: pythPriceAccountPk, + ixsSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + accumulatorWhitelist: whitelistPubkey, + accumulatorProgram: accumulatorUpdaterProgram.programId, + }) + .remainingAccounts(accumulatorPdaMetas) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), + ]) + .rpc({ + skipPreflight: true, + }); + + const pythPriceAccount = await mockCpiProg.account.priceAccount.fetch( + pythPriceAccountPk + ); + assert.isTrue(pythPriceAccount.price.eq(updatePriceParams.price)); + assert.isTrue(pythPriceAccount.priceExpo.eq(updatePriceParams.priceExpo)); + assert.isTrue(pythPriceAccount.ema.eq(updatePriceParams.ema)); + assert.isTrue(pythPriceAccount.emaExpo.eq(updatePriceParams.emaExpo)); + const accumulatorInputs = + await accumulatorUpdaterProgram.account.accumulatorInput.fetchMultiple( + accumulatorPdaMetas.map((m) => m.pubkey) + ); + const updatedAccumulatorPriceMessages = accumulatorInputs.map((ai) => { + return parseAccumulatorInput(ai); + }); + + console.log( + `updatedAccumulatorPriceMessages: ${JSON.stringify( + updatedAccumulatorPriceMessages, + null, + 2 + )}` + ); + updatedAccumulatorPriceMessages.forEach((pm) => { + assert.isTrue(pm.id.eq(addPriceParams.id)); + assert.isTrue(pm.price.eq(updatePriceParams.price)); + assert.isTrue(pm.priceExpo.eq(updatePriceParams.priceExpo)); + }); + }); }); +export const getAccumulatorPdaMetas = ( + pythAccount: anchor.web3.PublicKey, + schemas: number[] +): AccountMeta[] => { + const accumulatorPdaKeys = schemas.map((pythSchema) => { + return anchor.web3.PublicKey.findProgramAddressSync( + [ + mockCpiProg.programId.toBuffer(), + Buffer.from("accumulator"), + pythAccount.toBuffer(), + new anchor.BN(pythSchema).toArrayLike(Buffer, "le", 1), + ], + accumulatorUpdaterProgram.programId + )[0]; + }); + return accumulatorPdaKeys.map((pda) => { + return { + pubkey: pda, + isSigner: false, + isWritable: true, + }; + }); +}; + type AccumulatorInputHeader = IdlTypes["AccumulatorHeader"]; // Parses AccumulatorInput.data into a PriceAccount or PriceOnly object based on the @@ -228,7 +325,6 @@ function parseAccumulatorInput({ data: Buffer; }): AccumulatorPriceMessage { console.log(`header: ${JSON.stringify(header)}`); - assert.strictEqual(header.accountType, 3); if (header.accountSchema === 0) { console.log(`[full]data: ${data.toString("hex")}`); return parseFullPriceMessage(data);