Skip to content
6 changes: 6 additions & 0 deletions program/c/src/oracle/oracle.h
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,12 @@ typedef enum {
// key[1] price account [Signer writable]
// key[2] system program [readable]
e_cmd_resize_price_account,

// deletes a price account
// key[0] funding account [signer writable]
// key[1] product account [signer writable]
// key[2] price account [signer writable]
e_cmd_del_price,
} command_t;

typedef struct cmd_hdr
Expand Down
5 changes: 5 additions & 0 deletions program/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ solana-program = "=1.10.29"
bytemuck = "1.11.0"
thiserror = "1.0"

[dev-dependencies]
solana-program-test = "=1.10.29"
solana-sdk = "=1.10.29"
tokio = "1.14.1"

[features]
debug = []

Expand Down
3 changes: 3 additions & 0 deletions program/rust/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::c_oracle_header::{
command_t_e_cmd_add_product,
command_t_e_cmd_add_publisher,
command_t_e_cmd_agg_price,
command_t_e_cmd_del_price,
command_t_e_cmd_del_publisher,
command_t_e_cmd_init_mapping,
command_t_e_cmd_init_price,
Expand All @@ -27,6 +28,7 @@ use crate::rust_oracle::{
add_price,
add_product,
add_publisher,
del_price,
del_publisher,
init_mapping,
init_price,
Expand Down Expand Up @@ -72,6 +74,7 @@ pub fn process_instruction(
command_t_e_cmd_add_product => add_product(program_id, accounts, instruction_data),
command_t_e_cmd_upd_product => upd_product(program_id, accounts, instruction_data),
command_t_e_cmd_set_min_pub => set_min_pub(program_id, accounts, instruction_data),
command_t_e_cmd_del_price => del_price(program_id, accounts, instruction_data),
_ => Err(OracleError::UnrecognizedInstruction.into()),
}
}
60 changes: 52 additions & 8 deletions program/rust/src/rust_oracle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,17 @@ use bytemuck::{
use solana_program::account_info::AccountInfo;
use solana_program::clock::Clock;
use solana_program::entrypoint::ProgramResult;
use solana_program::program::invoke;
use solana_program::program_error::ProgramError;
use solana_program::program_memory::{
sol_memcpy,
sol_memset,
};
use solana_program::pubkey::Pubkey;
use solana_program::rent::Rent;
use solana_program::sysvar::Sysvar;


use crate::time_machine_types::PriceAccountWrapper;
use solana_program::program::invoke;
use solana_program::system_instruction::transfer;
use solana_program::system_program::check_id;

use solana_program::sysvar::Sysvar;

use crate::c_oracle_header::{
cmd_add_price_t,
Expand Down Expand Up @@ -57,8 +53,7 @@ use crate::deserialize::{
load_account_as_mut,
load_checked,
};
use crate::OracleError;

use crate::time_machine_types::PriceAccountWrapper;
use crate::utils::{
check_exponent_range,
check_valid_fresh_account,
Expand All @@ -73,6 +68,7 @@ use crate::utils::{
read_pc_str_t,
try_convert,
};
use crate::OracleError;

const PRICE_T_SIZE: usize = size_of::<pc_price_t>();
const PRICE_ACCOUNT_SIZE: usize = size_of::<PriceAccountWrapper>();
Expand Down Expand Up @@ -371,6 +367,54 @@ pub fn add_price(
Ok(())
}

/// Delete a price account. This function will remove the link between the price account and its
/// corresponding product account, then transfer any SOL in the price account to the funding
/// account. This function can only delete the first price account in the linked list of
/// price accounts for the given product.
///
/// Warning: This function is dangerous and will break any programs that depend on the deleted
/// price account!
pub fn del_price(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let [funding_account, product_account, price_account] = match accounts {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tuples over fixed length array literals + explicit return instead of wrapping/unwrapping an unnecessary Result.

Suggested change
let [funding_account, product_account, price_account] = match accounts {
let (funding_account, product_account, price_account) = match accounts {
[w, x, y] => (w, x, y),
_ => return Err(ProgramError::InvalidArgument),
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm yeah that does seem better. we've been doing it this way in a bunch of places already though, so i'm not going to change this right now.

[w, x, y] => Ok([w, x, y]),
_ => Err(ProgramError::InvalidArgument),
}?;

check_valid_funding_account(funding_account)?;
check_valid_signable_account(program_id, product_account, PC_PROD_ACC_SIZE as usize)?;
check_valid_signable_account(program_id, price_account, size_of::<pc_price_t>())?;

{
let cmd_args = load::<cmd_hdr_t>(instruction_data)?;
let mut product_data = load_checked::<pc_prod_t>(product_account, cmd_args.ver_)?;
let price_data = load_checked::<pc_price_t>(price_account, cmd_args.ver_)?;
pyth_assert(
pubkey_equal(&product_data.px_acc_, &price_account.key.to_bytes()),
ProgramError::InvalidArgument,
)?;

pyth_assert(
pubkey_equal(&price_data.prod_, &product_account.key.to_bytes()),
ProgramError::InvalidArgument,
)?;

pubkey_assign(&mut product_data.px_acc_, bytes_of(&price_data.next_));
}

// Zero out the balance of the price account to delete it.
// Note that you can't use the system program's transfer instruction to do this operation, as
// that instruction fails if the source account has any data.
let lamports = price_account.lamports();
**price_account.lamports.borrow_mut() = 0;
**funding_account.lamports.borrow_mut() += lamports;

Ok(())
}

pub fn init_price(
program_id: &Pubkey,
accounts: &[AccountInfo],
Expand Down
2 changes: 2 additions & 0 deletions program/rust/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod pyth_simulator;
mod test_add_mapping;
mod test_add_price;
mod test_add_product;
mod test_add_publisher;
mod test_del_price;
mod test_del_publisher;
mod test_init_mapping;
mod test_init_price;
Expand Down
232 changes: 232 additions & 0 deletions program/rust/src/tests/pyth_simulator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
use std::mem::size_of;

use bytemuck::{
bytes_of,
Pod,
};
use solana_program::hash::Hash;
use solana_program::instruction::{
AccountMeta,
Instruction,
};
use solana_program::pubkey::Pubkey;
use solana_program::rent::Rent;
use solana_program::system_instruction;
use solana_program_test::{
processor,
BanksClient,
BanksClientError,
ProgramTest,
ProgramTestBanksClientExt,
};
use solana_sdk::account::Account;
use solana_sdk::signature::{
Keypair,
Signer,
};
use solana_sdk::transaction::Transaction;

use crate::c_oracle_header::{
cmd_add_price_t,
cmd_hdr_t,
command_t_e_cmd_add_price,
command_t_e_cmd_add_product,
command_t_e_cmd_del_price,
command_t_e_cmd_init_mapping,
pc_map_table_t,
pc_price_t,
PC_PROD_ACC_SIZE,
PC_PTYPE_PRICE,
PC_VERSION,
};
use crate::deserialize::load;
use crate::processor::process_instruction;

/// Simulator for the state of the pyth program on Solana. You can run solana transactions against
/// this struct to test how pyth instructions execute in the Solana runtime.
pub struct PythSimulator {
program_id: Pubkey,
banks_client: BanksClient,
payer: Keypair,
/// Hash used to submit the last transaction. The hash must be advanced for each new
/// transaction; otherwise, replayed transactions in different states can return stale
/// results.
last_blockhash: Hash,
}

impl PythSimulator {
pub async fn new() -> PythSimulator {
let program_id = Pubkey::new_unique();
let (banks_client, payer, recent_blockhash) =
ProgramTest::new("pyth_oracle", program_id, processor!(process_instruction))
.start()
.await;

PythSimulator {
program_id,
banks_client,
payer,
last_blockhash: recent_blockhash,
}
}

/// Process a transaction containing `instruction` signed by `signers`.
/// The transaction is assumed to require `self.payer` to pay for and sign the transaction.
async fn process_ix(
&mut self,
instruction: Instruction,
signers: &Vec<&Keypair>,
) -> Result<(), BanksClientError> {
let mut transaction =
Transaction::new_with_payer(&[instruction], Some(&self.payer.pubkey()));

let blockhash = self
.banks_client
.get_new_latest_blockhash(&self.last_blockhash)
.await
.unwrap();
self.last_blockhash = blockhash;

transaction.partial_sign(&[&self.payer], self.last_blockhash);
transaction.partial_sign(signers, self.last_blockhash);

self.banks_client.process_transaction(transaction).await
}

/// Create an account owned by the pyth program containing `size` bytes.
/// The account will be created with enough lamports to be rent-exempt.
pub async fn create_pyth_account(&mut self, size: usize) -> Keypair {
let keypair = Keypair::new();
let rent = Rent::minimum_balance(&Rent::default(), size);
let instruction = system_instruction::create_account(
&self.payer.pubkey(),
&keypair.pubkey(),
rent,
size as u64,
&self.program_id,
);

self.process_ix(instruction, &vec![&keypair]).await.unwrap();

keypair
}

/// Initialize a mapping account (using the init_mapping instruction), returning the keypair
/// associated with the newly-created account.
pub async fn init_mapping(&mut self) -> Result<Keypair, BanksClientError> {
let mapping_keypair = self.create_pyth_account(size_of::<pc_map_table_t>()).await;

let cmd = cmd_hdr_t {
ver_: PC_VERSION,
cmd_: command_t_e_cmd_init_mapping as i32,
};
let instruction = Instruction::new_with_bytes(
self.program_id,
bytes_of(&cmd),
vec![
AccountMeta::new(self.payer.pubkey(), true),
AccountMeta::new(mapping_keypair.pubkey(), true),
],
);

self.process_ix(instruction, &vec![&mapping_keypair])
.await
.map(|_| mapping_keypair)
}

/// Initialize a product account and add it to an existing mapping account (using the
/// add_product instruction). Returns the keypair associated with the newly-created account.
pub async fn add_product(
&mut self,
mapping_keypair: &Keypair,
) -> Result<Keypair, BanksClientError> {
let product_keypair = self.create_pyth_account(PC_PROD_ACC_SIZE as usize).await;

let cmd = cmd_hdr_t {
ver_: PC_VERSION,
cmd_: command_t_e_cmd_add_product as i32,
};
let instruction = Instruction::new_with_bytes(
self.program_id,
bytes_of(&cmd),
vec![
AccountMeta::new(self.payer.pubkey(), true),
AccountMeta::new(mapping_keypair.pubkey(), true),
AccountMeta::new(product_keypair.pubkey(), true),
],
);

self.process_ix(instruction, &vec![&mapping_keypair, &product_keypair])
.await
.map(|_| product_keypair)
}

/// Initialize a price account and add it to an existing product account (using the add_price
/// instruction). Returns the keypair associated with the newly-created account.
pub async fn add_price(
&mut self,
product_keypair: &Keypair,
expo: i32,
) -> Result<Keypair, BanksClientError> {
let price_keypair = self.create_pyth_account(size_of::<pc_price_t>()).await;

let cmd = cmd_add_price_t {
ver_: PC_VERSION,
cmd_: command_t_e_cmd_add_price as i32,
expo_: expo,
ptype_: PC_PTYPE_PRICE,
};
let instruction = Instruction::new_with_bytes(
self.program_id,
bytes_of(&cmd),
vec![
AccountMeta::new(self.payer.pubkey(), true),
AccountMeta::new(product_keypair.pubkey(), true),
AccountMeta::new(price_keypair.pubkey(), true),
],
);

self.process_ix(instruction, &vec![&product_keypair, &price_keypair])
.await
.map(|_| price_keypair)
}

/// Delete a price account from an existing product account (using the del_price instruction).
pub async fn del_price(
&mut self,
product_keypair: &Keypair,
price_keypair: &Keypair,
) -> Result<(), BanksClientError> {
let cmd = cmd_hdr_t {
ver_: PC_VERSION,
cmd_: command_t_e_cmd_del_price as i32,
};
let instruction = Instruction::new_with_bytes(
self.program_id,
bytes_of(&cmd),
vec![
AccountMeta::new(self.payer.pubkey(), true),
AccountMeta::new(product_keypair.pubkey(), true),
AccountMeta::new(price_keypair.pubkey(), true),
],
);

self.process_ix(instruction, &vec![&product_keypair, &price_keypair])
.await
}

/// Get the account at `key`. Returns `None` if no such account exists.
pub async fn get_account(&mut self, key: Pubkey) -> Option<Account> {
self.banks_client.get_account(key).await.unwrap()
}

/// Get the content of an account as a value of type `T`. This function returns a copy of the
/// account data -- you cannot mutate the result to mutate the on-chain account data.
/// Returns None if the account does not exist. Panics if the account data cannot be read as a
/// `T`.
pub async fn get_account_data_as<T: Pod>(&mut self, key: Pubkey) -> Option<T> {
self.get_account(key)
.await
.map(|x| load::<T>(&x.data).unwrap().clone())
}
}
Loading