[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 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
|
||||||
/// AccumulatorInput(s) are derived from
|
/// while calling this function in the same order as elements.
|
||||||
/// * `data` - Vec of AccumulatorInput account data
|
///
|
||||||
/// * `account_type` - Marker to indicate base_account account_type
|
///
|
||||||
/// * `account_schemas` - Vec of markers to indicate schemas for
|
/// * `base_account_key` - Pubkey of the original account the
|
||||||
/// AccumulatorInputs. In same respective
|
/// AccumulatorInput(s) are derived from
|
||||||
/// order as data
|
/// * `values` - Vec of (schema, account_data) in same respective
|
||||||
pub fn create_inputs<'info>(
|
/// order `ctx.remaining_accounts`
|
||||||
ctx: Context<'_, '_, '_, 'info, CreateInputs<'info>>,
|
pub fn put_all<'info>(
|
||||||
base_account: Pubkey,
|
ctx: Context<'_, '_, '_, 'info, PutAll<'info>>,
|
||||||
data: Vec<Vec<u8>>,
|
base_account_key: Pubkey,
|
||||||
account_type: u32,
|
values: Vec<InputSchemaAndData>,
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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],
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// invoke cpi call using anchor
|
/// Updates a `PriceAccount` with the given parameters
|
||||||
fn invoke_cpi_anchor(
|
pub fn update_price<'info>(
|
||||||
ctx: Context<'_, '_, '_, 'info, AddPrice<'info>>,
|
ctx: Context<'_, '_, '_, 'info, UpdatePrice<'info>>,
|
||||||
account_data: Vec<Vec<u8>>,
|
params: UpdatePriceParams,
|
||||||
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(), ¶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]
|
#[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:?}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::PythAccountType;
|
use crate::state::PythAccountType;
|
||||||
|
|
||||||
pub mod price;
|
pub mod price;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use {
|
use {
|
||||||
crate::{
|
crate::{
|
||||||
message::AccumulatorSerializer,
|
message::AccumulatorSerializer,
|
||||||
PriceAccount,
|
state::PriceAccount,
|
||||||
},
|
},
|
||||||
anchor_lang::prelude::*,
|
anchor_lang::prelude::*,
|
||||||
bytemuck::{
|
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 { 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);
|
||||||
|
|
Loading…
Reference in New Issue