[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
This commit is contained in:
swimricky 2023-04-05 16:33:12 -07:00 committed by GitHub
parent c3b2322ac0
commit 4502c6dcf9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 867 additions and 512 deletions

View File

@ -0,0 +1,3 @@
pub use put_all::*;
mod put_all;

View File

@ -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<u8>,
}
pub fn put_all<'info>(
ctx: Context<'_, '_, '_, 'info, PutAll<'info>>,
base_account_key: Pubkey,
values: Vec<InputSchemaAndData>,
) -> 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 = <AccumulatorInput as AccountDeserialize>::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(())
}
}

View File

@ -1,15 +1,12 @@
mod instructions;
mod macros; mod macros;
mod state;
use anchor_lang::{
prelude::*, use {
solana_program::sysvar::{ anchor_lang::prelude::*,
self, instructions::*,
instructions::get_instruction_relative, state::*,
},
system_program::{
self,
CreateAccount,
},
}; };
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
@ -18,6 +15,9 @@ declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
pub mod accumulator_updater { pub mod accumulator_updater {
use super::*; 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<Initialize>, authority: Pubkey) -> Result<()> { pub fn initialize(ctx: Context<Initialize>, authority: Pubkey) -> Result<()> {
require_keys_neq!(authority, Pubkey::default()); require_keys_neq!(authority, Pubkey::default());
let whitelist = &mut ctx.accounts.whitelist; let whitelist = &mut ctx.accounts.whitelist;
@ -26,6 +26,10 @@ pub mod accumulator_updater {
Ok(()) 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( pub fn set_allowed_programs(
ctx: Context<UpdateWhitelist>, ctx: Context<UpdateWhitelist>,
allowed_programs: Vec<Pubkey>, allowed_programs: Vec<Pubkey>,
@ -36,6 +40,7 @@ pub mod accumulator_updater {
Ok(()) Ok(())
} }
/// Sets the new authority for the whitelist
pub fn update_whitelist_authority( pub fn update_whitelist_authority(
ctx: Context<UpdateWhitelist>, ctx: Context<UpdateWhitelist>,
new_authority: Pubkey, new_authority: Pubkey,
@ -46,125 +51,25 @@ pub mod accumulator_updater {
Ok(()) 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 /// 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 /// AccumulatorInput(s) are derived from
/// * `data` - Vec of AccumulatorInput account data /// * `values` - Vec of (schema, account_data) in same respective
/// * `account_type` - Marker to indicate base_account account_type /// order `ctx.remaining_accounts`
/// * `account_schemas` - Vec of markers to indicate schemas for pub fn put_all<'info>(
/// AccumulatorInputs. In same respective ctx: Context<'_, '_, '_, 'info, PutAll<'info>>,
/// order as data base_account_key: Pubkey,
pub fn create_inputs<'info>( values: Vec<InputSchemaAndData>,
ctx: Context<'_, '_, '_, 'info, CreateInputs<'info>>,
base_account: Pubkey,
data: Vec<Vec<u8>>,
account_type: u32,
account_schemas: Vec<u8>,
) -> Result<()> { ) -> Result<()> {
let cpi_caller = ctx.accounts.whitelist_verifier.is_allowed()?; instructions::put_all(ctx, base_account_key, values)
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<Pubkey>,
}
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<Pubkey> {
let instruction = get_instruction_relative(0, &self.ixs_sysvar.to_account_info())?;
Ok(instruction.program_id)
}
pub fn is_allowed(&self) -> Result<Pubkey> {
let cpi_caller = self.get_cpi_caller()?;
let whitelist = &self.whitelist;
require!(
whitelist.allowed_programs.contains(&cpi_caller),
AccumulatorUpdaterError::CallerNotAllowed
);
Ok(cpi_caller)
} }
} }
@ -201,123 +106,6 @@ pub struct UpdateWhitelist<'info> {
} }
#[derive(Accounts)]
#[instruction(base_account: Pubkey, data: Vec<Vec<u8>>, 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<Account<'info, AccumulatorInput>>,
}
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<u8> for resizing?
pub data: Vec<u8>,
}
impl AccumulatorInput {
pub fn get_initial_size(data: &Vec<u8>) -> usize {
AccumulatorHeader::SIZE + 4 + data.len()
}
pub fn new(header: AccumulatorHeader, data: Vec<u8>) -> 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] #[error_code]
pub enum AccumulatorUpdaterError { pub enum AccumulatorUpdaterError {
#[msg("CPI Caller not allowed")] #[msg("CPI Caller not allowed")]
@ -334,4 +122,10 @@ pub enum AccumulatorUpdaterError {
InvalidAllowedProgram, InvalidAllowedProgram,
#[msg("Maximum number of allowed programs exceeded")] #[msg("Maximum number of allowed programs exceeded")]
MaximumAllowedProgramsExceeded, MaximumAllowedProgramsExceeded,
#[msg("Invalid PDA")]
InvalidPDA,
#[msg("Update data exceeds current length")]
CurrentDataLengthExceeded,
#[msg("Accumulator Input not writable")]
AccumulatorInputNotWritable,
} }

View File

@ -1,25 +1,12 @@
#[macro_export] #[macro_export]
macro_rules! accumulator_acc_seeds { macro_rules! accumulator_input_seeds {
($cpi_caller_pid:expr, $base_account:expr, $account_type:expr) => { ($accumulator_input:expr, $cpi_caller_pid:expr, $base_account:expr) => {
&[ &[
$cpi_caller_pid.as_ref(), $cpi_caller_pid.as_ref(),
b"accumulator".as_ref(), b"accumulator".as_ref(),
$base_account.as_ref(), $base_account.as_ref(),
&$account_type.to_le_bytes(), &$accumulator_input.header.account_schema.to_le_bytes(),
] &[$accumulator_input.header.bump],
};
}
#[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],
] ]
}; };
} }

View File

@ -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<u8>,
}
impl AccumulatorInput {
pub fn size(data: &Vec<u8>) -> usize {
AccumulatorHeader::SIZE + 4 + data.len()
}
pub fn new(header: AccumulatorHeader, data: Vec<u8>) -> Self {
Self { header, data }
}
pub fn update(&mut self, new_data: Vec<u8>) {
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<Pubkey> {
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,
}
}
}

View File

@ -0,0 +1,7 @@
pub use {
accumulator_input::*,
whitelist::*,
};
mod accumulator_input;
mod whitelist;

View File

@ -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<Pubkey>,
}
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<Pubkey> {
let instruction = get_instruction_relative(0, &self.ixs_sysvar.to_account_info())?;
Ok(instruction.program_id)
}
pub fn is_allowed(&self) -> Result<Pubkey> {
let cpi_caller = self.get_cpi_caller()?;
let whitelist = &self.whitelist;
require!(
whitelist.allowed_programs.contains(&cpi_caller),
AccumulatorUpdaterError::CallerNotAllowed
);
Ok(cpi_caller)
}
}

View File

@ -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<u8>> = 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::<Vec<(u8, Vec<u8>)>>();
// 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<u8>)>,
) -> 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::<Vec<_>>(),
);
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(), &params.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
}

View File

@ -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";

View File

@ -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::<Vec<u8>>();
let values = schemas
.into_iter()
.map(|s| s.to_u8())
.zip(account_data)
.collect::<Vec<(u8, Vec<u8>)>>();
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<u8>)>,
// account_data: Vec<Vec<u8>>,
// account_schemas: Vec<u8>,
) -> 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::<Vec<_>>(),
);
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(())
}
}

View File

@ -1,236 +1,33 @@
use { use {
accumulator_updater::{ anchor_lang::prelude::*,
cpi::accounts as AccumulatorUpdaterCpiAccts, instructions::*,
program::AccumulatorUpdater as AccumulatorUpdaterProgram,
},
anchor_lang::{
prelude::*,
solana_program::{
hash::hashv,
sysvar,
},
},
message::{
get_schemas,
price::*,
AccumulatorSerializer,
},
}; };
pub mod instructions;
pub mod message; pub mod message;
mod state;
declare_id!("Dg5PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); declare_id!("Dg5PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program] #[program]
pub mod mock_cpi_caller { pub mod mock_cpi_caller {
use super::*; use super::*;
/// Creates a `PriceAccount` with the given parameters
pub fn add_price<'info>( pub fn add_price<'info>(
ctx: Context<'_, '_, '_, 'info, AddPrice<'info>>, ctx: Context<'_, '_, '_, 'info, AddPrice<'info>>,
params: AddPriceParams, params: AddPriceParams,
) -> Result<()> { ) -> Result<()> {
let mut account_data: Vec<Vec<u8>> = vec![]; instructions::add_price(ctx, params)
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);
} }
/// Updates a `PriceAccount` with the given parameters
let account_schemas = schemas.into_iter().map(|s| s.to_u8()).collect::<Vec<u8>>(); pub fn update_price<'info>(
ctx: Context<'_, '_, '_, 'info, UpdatePrice<'info>>,
// 44444 compute units params: UpdatePriceParams,
// 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
}
/// invoke cpi call using anchor
fn invoke_cpi_anchor(
ctx: Context<'_, '_, '_, 'info, AddPrice<'info>>,
account_data: Vec<Vec<u8>>,
account_type: PythAccountType,
account_schemas: Vec<u8>,
) -> Result<()> { ) -> Result<()> {
accumulator_updater::cpi::create_inputs( instructions::update_price(ctx, params)
ctx.accounts.create_inputs_ctx(ctx.remaining_accounts),
ctx.accounts.pyth_price_account.key(),
account_data,
account_type.to_u32(),
account_schemas,
)?;
Ok(())
} }
/// invoke cpi call using solana
fn invoke_cpi_solana(
ctx: Context<'_, '_, '_, 'info, AddPrice<'info>>,
account_data: Vec<Vec<u8>>,
account_type: PythAccountType,
account_schemas: Vec<u8>,
) -> 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::<Vec<_>>(),
);
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(), &params.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] #[test]
fn ix_discriminator() { fn ix_discriminator() {
let a = &(accumulator_updater::instruction::CreateInputs { let a = &(accumulator_updater::instruction::PutAll {
base_account: anchor_lang::prelude::Pubkey::default(), base_account_key: anchor_lang::prelude::Pubkey::default(),
data: vec![], values: vec![],
account_type: 0,
account_schemas: vec![],
} }
.data()[..8]); .data()[..8]);
let sighash = sighash("global", "create_inputs"); let sighash = sighash("global", ACCUMULATOR_UPDATER_IX_NAME);
println!( println!(
r" r"
a: {a:?} a: {a:?}

View File

@ -1,4 +1,4 @@
use crate::PythAccountType; use crate::state::PythAccountType;
pub mod price; pub mod price;

View File

@ -1,7 +1,7 @@
use { use {
crate::{ crate::{
message::AccumulatorSerializer, message::AccumulatorSerializer,
PriceAccount, state::PriceAccount,
}, },
anchor_lang::prelude::*, anchor_lang::prelude::*,
bytemuck::{ bytemuck::{

View File

@ -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
}
}

View File

@ -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;
}

View File

@ -4,7 +4,7 @@ import { AccumulatorUpdater } from "../target/types/accumulator_updater";
import { MockCpiCaller } from "../target/types/mock_cpi_caller"; import { MockCpiCaller } from "../target/types/mock_cpi_caller";
import lumina from "@lumina-dev/test"; import lumina from "@lumina-dev/test";
import { assert } from "chai"; import { assert } from "chai";
import { ComputeBudgetProgram } from "@solana/web3.js"; import { AccountMeta, ComputeBudgetProgram } from "@solana/web3.js";
import bs58 from "bs58"; import bs58 from "bs58";
// Enables tool that runs in local browser for easier debugging of // 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<MockCpiCaller>; const mockCpiProg = anchor.workspace.MockCpiCaller as Program<MockCpiCaller>;
let whitelistAuthority = anchor.web3.Keypair.generate(); 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", () => { describe("accumulator_updater", () => {
// Configure the client to use the local cluster. // Configure the client to use the local cluster.
let provider = anchor.AnchorProvider.env(); let provider = anchor.AnchorProvider.env();
@ -84,14 +103,6 @@ describe("accumulator_updater", () => {
}); });
it("Mock CPI program - AddPrice", async () => { 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 const mockCpiCallerAddPriceTxPubkeys = await mockCpiProg.methods
.addPrice(addPriceParams) .addPrice(addPriceParams)
.accounts({ .accounts({
@ -102,17 +113,19 @@ describe("accumulator_updater", () => {
}) })
.pubkeys(); .pubkeys();
const accumulatorPdas = [0, 1].map((pythSchema) => { const accumulatorPdaKeys = PRICE_SCHEMAS.map((pythSchema) => {
const [pda] = anchor.web3.PublicKey.findProgramAddressSync( return anchor.web3.PublicKey.findProgramAddressSync(
[ [
mockCpiProg.programId.toBuffer(), mockCpiProg.programId.toBuffer(),
Buffer.from("accumulator"), Buffer.from("accumulator"),
mockCpiCallerAddPriceTxPubkeys.pythPriceAccount.toBuffer(), // mockCpiCallerAddPriceTxPubkeys.pythPriceAccount.toBuffer(),
pythPriceAccountPk.toBuffer(),
new anchor.BN(pythSchema).toArrayLike(Buffer, "le", 1), new anchor.BN(pythSchema).toArrayLike(Buffer, "le", 1),
], ],
accumulatorUpdaterProgram.programId accumulatorUpdaterProgram.programId
); )[0];
console.log(`pda for pyth schema ${pythSchema}: ${pda.toString()}`); });
const accumulatorPdaMetas = accumulatorPdaKeys.map((pda) => {
return { return {
pubkey: pda, pubkey: pda,
isSigner: false, isSigner: false,
@ -126,7 +139,7 @@ describe("accumulator_updater", () => {
.accounts({ .accounts({
...mockCpiCallerAddPriceTxPubkeys, ...mockCpiCallerAddPriceTxPubkeys,
}) })
.remainingAccounts(accumulatorPdas) .remainingAccounts(accumulatorPdaMetas)
.prepare(); .prepare();
console.log( console.log(
@ -153,7 +166,7 @@ describe("accumulator_updater", () => {
.accounts({ .accounts({
...mockCpiCallerAddPriceTxPubkeys, ...mockCpiCallerAddPriceTxPubkeys,
}) })
.remainingAccounts(accumulatorPdas) .remainingAccounts(accumulatorPdaMetas)
.preInstructions([ .preInstructions([
ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }),
]) ])
@ -165,28 +178,31 @@ describe("accumulator_updater", () => {
const pythPriceAccount = await provider.connection.getAccountInfo( const pythPriceAccount = await provider.connection.getAccountInfo(
mockCpiCallerAddPriceTxPubkeys.pythPriceAccount mockCpiCallerAddPriceTxPubkeys.pythPriceAccount
); );
console.log(`pythPriceAccount: ${pythPriceAccount.data.toString("hex")}`); const pythPriceAcct = {
const accumulatorInputKeys = accumulatorPdas.map((a) => a.pubkey); ...pythPriceAccount,
data: pythPriceAccount.data.toString("hex"),
};
console.log(`pythPriceAccount: ${JSON.stringify(pythPriceAcct)}`);
const accumulatorInputs = const accumulatorInputs =
await accumulatorUpdaterProgram.account.accumulatorInput.fetchMultiple( await accumulatorUpdaterProgram.account.accumulatorInput.fetchMultiple(
accumulatorInputKeys accumulatorPdaKeys
); );
const accumulatorPriceAccounts = accumulatorInputs.map((ai) => { const accumulatorPriceMessages = accumulatorInputs.map((ai) => {
return parseAccumulatorInput(ai); return parseAccumulatorInput(ai);
}); });
console.log( console.log(
`accumulatorPriceAccounts: ${JSON.stringify( `accumulatorPriceMessages: ${JSON.stringify(
accumulatorPriceAccounts, accumulatorPriceMessages,
null, null,
2 2
)}` )}`
); );
accumulatorPriceAccounts.forEach((pa) => { accumulatorPriceMessages.forEach((pm) => {
assert.isTrue(pa.id.eq(addPriceParams.id)); assert.isTrue(pm.id.eq(addPriceParams.id));
assert.isTrue(pa.price.eq(addPriceParams.price)); assert.isTrue(pm.price.eq(addPriceParams.price));
assert.isTrue(pa.priceExpo.eq(addPriceParams.priceExpo)); assert.isTrue(pm.priceExpo.eq(addPriceParams.priceExpo));
}); });
let discriminator = let discriminator =
@ -207,15 +223,96 @@ describe("accumulator_updater", () => {
], ],
} }
); );
const accumulatorInputKeyStrings = accumulatorInputKeys.map((k) => const accumulatorInputKeyStrings = accumulatorPdaKeys.map((k) =>
k.toString() k.toString()
); );
accumulatorAccounts.forEach((a) => { accumulatorAccounts.forEach((a) => {
assert.isTrue(accumulatorInputKeyStrings.includes(a.pubkey.toString())); 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<AccumulatorUpdater>["AccumulatorHeader"]; type AccumulatorInputHeader = IdlTypes<AccumulatorUpdater>["AccumulatorHeader"];
// Parses AccumulatorInput.data into a PriceAccount or PriceOnly object based on the // Parses AccumulatorInput.data into a PriceAccount or PriceOnly object based on the
@ -228,7 +325,6 @@ function parseAccumulatorInput({
data: Buffer; data: Buffer;
}): AccumulatorPriceMessage { }): AccumulatorPriceMessage {
console.log(`header: ${JSON.stringify(header)}`); console.log(`header: ${JSON.stringify(header)}`);
assert.strictEqual(header.accountType, 3);
if (header.accountSchema === 0) { if (header.accountSchema === 0) {
console.log(`[full]data: ${data.toString("hex")}`); console.log(`[full]data: ${data.toString("hex")}`);
return parseFullPriceMessage(data); return parseFullPriceMessage(data);