[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:
parent
c3b2322ac0
commit
4502c6dcf9
|
@ -0,0 +1,3 @@
|
|||
pub use put_all::*;
|
||||
|
||||
mod put_all;
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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<Initialize>, 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<UpdateWhitelist>,
|
||||
allowed_programs: Vec<Pubkey>,
|
||||
|
@ -36,6 +40,7 @@ pub mod accumulator_updater {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the new authority for the whitelist
|
||||
pub fn update_whitelist_authority(
|
||||
ctx: Context<UpdateWhitelist>,
|
||||
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<Vec<u8>>,
|
||||
account_type: u32,
|
||||
account_schemas: Vec<u8>,
|
||||
/// 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<InputSchemaAndData>,
|
||||
) -> 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<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)
|
||||
instructions::put_all(ctx, base_account_key, values)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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]
|
||||
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,
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
]
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
pub use {
|
||||
accumulator_input::*,
|
||||
whitelist::*,
|
||||
};
|
||||
|
||||
mod accumulator_input;
|
||||
mod whitelist;
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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(), ¶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
|
||||
}
|
|
@ -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";
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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<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 account_schemas = schemas.into_iter().map(|s| s.to_u8()).collect::<Vec<u8>>();
|
||||
|
||||
// 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<Vec<u8>>,
|
||||
account_type: PythAccountType,
|
||||
account_schemas: Vec<u8>,
|
||||
/// 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<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(), ¶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:?}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::PythAccountType;
|
||||
use crate::state::PythAccountType;
|
||||
|
||||
pub mod price;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use {
|
||||
crate::{
|
||||
message::AccumulatorSerializer,
|
||||
PriceAccount,
|
||||
state::PriceAccount,
|
||||
},
|
||||
anchor_lang::prelude::*,
|
||||
bytemuck::{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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<MockCpiCaller>;
|
||||
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<AccumulatorUpdater>["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);
|
||||
|
|
Loading…
Reference in New Issue