Skip to content

Commit afd429e

Browse files
jayantkJayant Krishnamurthy
and
Jayant Krishnamurthy
authored
Command to delete price accounts (#260)
* deleting price accounts * ok * fix build * rut test * fix * trying to write a test * trying to write tests * trying to write tests * it works * it works * better comments * integration tests * jeez * fix this * doc * ok weird that this name matters Co-authored-by: Jayant Krishnamurthy <[email protected]>
1 parent ca645b7 commit afd429e

File tree

8 files changed

+354
-10
lines changed

8 files changed

+354
-10
lines changed

program/c/src/oracle/oracle.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,12 @@ typedef enum {
269269
// key[1] price account [Signer writable]
270270
// key[2] system program [readable]
271271
e_cmd_resize_price_account,
272+
273+
// deletes a price account
274+
// key[0] funding account [signer writable]
275+
// key[1] product account [signer writable]
276+
// key[2] price account [signer writable]
277+
e_cmd_del_price,
272278
} command_t;
273279

274280
typedef struct cmd_hdr

program/rust/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ solana-program = "=1.10.29"
1313
bytemuck = "1.11.0"
1414
thiserror = "1.0"
1515

16+
[dev-dependencies]
17+
solana-program-test = "=1.10.29"
18+
solana-sdk = "=1.10.29"
19+
tokio = "1.14.1"
20+
1621
[features]
1722
debug = []
1823

program/rust/src/processor.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::c_oracle_header::{
55
command_t_e_cmd_add_product,
66
command_t_e_cmd_add_publisher,
77
command_t_e_cmd_agg_price,
8+
command_t_e_cmd_del_price,
89
command_t_e_cmd_del_publisher,
910
command_t_e_cmd_init_mapping,
1011
command_t_e_cmd_init_price,
@@ -27,6 +28,7 @@ use crate::rust_oracle::{
2728
add_price,
2829
add_product,
2930
add_publisher,
31+
del_price,
3032
del_publisher,
3133
init_mapping,
3234
init_price,
@@ -72,6 +74,7 @@ pub fn process_instruction(
7274
command_t_e_cmd_add_product => add_product(program_id, accounts, instruction_data),
7375
command_t_e_cmd_upd_product => upd_product(program_id, accounts, instruction_data),
7476
command_t_e_cmd_set_min_pub => set_min_pub(program_id, accounts, instruction_data),
77+
command_t_e_cmd_del_price => del_price(program_id, accounts, instruction_data),
7578
_ => Err(OracleError::UnrecognizedInstruction.into()),
7679
}
7780
}

program/rust/src/rust_oracle.rs

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,17 @@ use bytemuck::{
1010
use solana_program::account_info::AccountInfo;
1111
use solana_program::clock::Clock;
1212
use solana_program::entrypoint::ProgramResult;
13+
use solana_program::program::invoke;
1314
use solana_program::program_error::ProgramError;
1415
use solana_program::program_memory::{
1516
sol_memcpy,
1617
sol_memset,
1718
};
1819
use solana_program::pubkey::Pubkey;
1920
use solana_program::rent::Rent;
20-
use solana_program::sysvar::Sysvar;
21-
22-
23-
use crate::time_machine_types::PriceAccountWrapper;
24-
use solana_program::program::invoke;
2521
use solana_program::system_instruction::transfer;
2622
use solana_program::system_program::check_id;
27-
23+
use solana_program::sysvar::Sysvar;
2824

2925
use crate::c_oracle_header::{
3026
cmd_add_price_t,
@@ -57,8 +53,7 @@ use crate::deserialize::{
5753
load_account_as_mut,
5854
load_checked,
5955
};
60-
use crate::OracleError;
61-
56+
use crate::time_machine_types::PriceAccountWrapper;
6257
use crate::utils::{
6358
check_exponent_range,
6459
check_valid_fresh_account,
@@ -73,6 +68,7 @@ use crate::utils::{
7368
read_pc_str_t,
7469
try_convert,
7570
};
71+
use crate::OracleError;
7672

7773
const PRICE_T_SIZE: usize = size_of::<pc_price_t>();
7874
const PRICE_ACCOUNT_SIZE: usize = size_of::<PriceAccountWrapper>();
@@ -371,6 +367,54 @@ pub fn add_price(
371367
Ok(())
372368
}
373369

370+
/// Delete a price account. This function will remove the link between the price account and its
371+
/// corresponding product account, then transfer any SOL in the price account to the funding
372+
/// account. This function can only delete the first price account in the linked list of
373+
/// price accounts for the given product.
374+
///
375+
/// Warning: This function is dangerous and will break any programs that depend on the deleted
376+
/// price account!
377+
pub fn del_price(
378+
program_id: &Pubkey,
379+
accounts: &[AccountInfo],
380+
instruction_data: &[u8],
381+
) -> ProgramResult {
382+
let [funding_account, product_account, price_account] = match accounts {
383+
[w, x, y] => Ok([w, x, y]),
384+
_ => Err(ProgramError::InvalidArgument),
385+
}?;
386+
387+
check_valid_funding_account(funding_account)?;
388+
check_valid_signable_account(program_id, product_account, PC_PROD_ACC_SIZE as usize)?;
389+
check_valid_signable_account(program_id, price_account, size_of::<pc_price_t>())?;
390+
391+
{
392+
let cmd_args = load::<cmd_hdr_t>(instruction_data)?;
393+
let mut product_data = load_checked::<pc_prod_t>(product_account, cmd_args.ver_)?;
394+
let price_data = load_checked::<pc_price_t>(price_account, cmd_args.ver_)?;
395+
pyth_assert(
396+
pubkey_equal(&product_data.px_acc_, &price_account.key.to_bytes()),
397+
ProgramError::InvalidArgument,
398+
)?;
399+
400+
pyth_assert(
401+
pubkey_equal(&price_data.prod_, &product_account.key.to_bytes()),
402+
ProgramError::InvalidArgument,
403+
)?;
404+
405+
pubkey_assign(&mut product_data.px_acc_, bytes_of(&price_data.next_));
406+
}
407+
408+
// Zero out the balance of the price account to delete it.
409+
// Note that you can't use the system program's transfer instruction to do this operation, as
410+
// that instruction fails if the source account has any data.
411+
let lamports = price_account.lamports();
412+
**price_account.lamports.borrow_mut() = 0;
413+
**funding_account.lamports.borrow_mut() += lamports;
414+
415+
Ok(())
416+
}
417+
374418
pub fn init_price(
375419
program_id: &Pubkey,
376420
accounts: &[AccountInfo],

program/rust/src/tests/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
mod pyth_simulator;
12
mod test_add_mapping;
23
mod test_add_price;
34
mod test_add_product;
45
mod test_add_publisher;
6+
mod test_del_price;
57
mod test_del_publisher;
68
mod test_init_mapping;
79
mod test_init_price;
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
use std::mem::size_of;
2+
3+
use bytemuck::{
4+
bytes_of,
5+
Pod,
6+
};
7+
use solana_program::hash::Hash;
8+
use solana_program::instruction::{
9+
AccountMeta,
10+
Instruction,
11+
};
12+
use solana_program::pubkey::Pubkey;
13+
use solana_program::rent::Rent;
14+
use solana_program::system_instruction;
15+
use solana_program_test::{
16+
processor,
17+
BanksClient,
18+
BanksClientError,
19+
ProgramTest,
20+
ProgramTestBanksClientExt,
21+
};
22+
use solana_sdk::account::Account;
23+
use solana_sdk::signature::{
24+
Keypair,
25+
Signer,
26+
};
27+
use solana_sdk::transaction::Transaction;
28+
29+
use crate::c_oracle_header::{
30+
cmd_add_price_t,
31+
cmd_hdr_t,
32+
command_t_e_cmd_add_price,
33+
command_t_e_cmd_add_product,
34+
command_t_e_cmd_del_price,
35+
command_t_e_cmd_init_mapping,
36+
pc_map_table_t,
37+
pc_price_t,
38+
PC_PROD_ACC_SIZE,
39+
PC_PTYPE_PRICE,
40+
PC_VERSION,
41+
};
42+
use crate::deserialize::load;
43+
use crate::processor::process_instruction;
44+
45+
/// Simulator for the state of the pyth program on Solana. You can run solana transactions against
46+
/// this struct to test how pyth instructions execute in the Solana runtime.
47+
pub struct PythSimulator {
48+
program_id: Pubkey,
49+
banks_client: BanksClient,
50+
payer: Keypair,
51+
/// Hash used to submit the last transaction. The hash must be advanced for each new
52+
/// transaction; otherwise, replayed transactions in different states can return stale
53+
/// results.
54+
last_blockhash: Hash,
55+
}
56+
57+
impl PythSimulator {
58+
pub async fn new() -> PythSimulator {
59+
let program_id = Pubkey::new_unique();
60+
let (banks_client, payer, recent_blockhash) =
61+
ProgramTest::new("pyth_oracle", program_id, processor!(process_instruction))
62+
.start()
63+
.await;
64+
65+
PythSimulator {
66+
program_id,
67+
banks_client,
68+
payer,
69+
last_blockhash: recent_blockhash,
70+
}
71+
}
72+
73+
/// Process a transaction containing `instruction` signed by `signers`.
74+
/// The transaction is assumed to require `self.payer` to pay for and sign the transaction.
75+
async fn process_ix(
76+
&mut self,
77+
instruction: Instruction,
78+
signers: &Vec<&Keypair>,
79+
) -> Result<(), BanksClientError> {
80+
let mut transaction =
81+
Transaction::new_with_payer(&[instruction], Some(&self.payer.pubkey()));
82+
83+
let blockhash = self
84+
.banks_client
85+
.get_new_latest_blockhash(&self.last_blockhash)
86+
.await
87+
.unwrap();
88+
self.last_blockhash = blockhash;
89+
90+
transaction.partial_sign(&[&self.payer], self.last_blockhash);
91+
transaction.partial_sign(signers, self.last_blockhash);
92+
93+
self.banks_client.process_transaction(transaction).await
94+
}
95+
96+
/// Create an account owned by the pyth program containing `size` bytes.
97+
/// The account will be created with enough lamports to be rent-exempt.
98+
pub async fn create_pyth_account(&mut self, size: usize) -> Keypair {
99+
let keypair = Keypair::new();
100+
let rent = Rent::minimum_balance(&Rent::default(), size);
101+
let instruction = system_instruction::create_account(
102+
&self.payer.pubkey(),
103+
&keypair.pubkey(),
104+
rent,
105+
size as u64,
106+
&self.program_id,
107+
);
108+
109+
self.process_ix(instruction, &vec![&keypair]).await.unwrap();
110+
111+
keypair
112+
}
113+
114+
/// Initialize a mapping account (using the init_mapping instruction), returning the keypair
115+
/// associated with the newly-created account.
116+
pub async fn init_mapping(&mut self) -> Result<Keypair, BanksClientError> {
117+
let mapping_keypair = self.create_pyth_account(size_of::<pc_map_table_t>()).await;
118+
119+
let cmd = cmd_hdr_t {
120+
ver_: PC_VERSION,
121+
cmd_: command_t_e_cmd_init_mapping as i32,
122+
};
123+
let instruction = Instruction::new_with_bytes(
124+
self.program_id,
125+
bytes_of(&cmd),
126+
vec![
127+
AccountMeta::new(self.payer.pubkey(), true),
128+
AccountMeta::new(mapping_keypair.pubkey(), true),
129+
],
130+
);
131+
132+
self.process_ix(instruction, &vec![&mapping_keypair])
133+
.await
134+
.map(|_| mapping_keypair)
135+
}
136+
137+
/// Initialize a product account and add it to an existing mapping account (using the
138+
/// add_product instruction). Returns the keypair associated with the newly-created account.
139+
pub async fn add_product(
140+
&mut self,
141+
mapping_keypair: &Keypair,
142+
) -> Result<Keypair, BanksClientError> {
143+
let product_keypair = self.create_pyth_account(PC_PROD_ACC_SIZE as usize).await;
144+
145+
let cmd = cmd_hdr_t {
146+
ver_: PC_VERSION,
147+
cmd_: command_t_e_cmd_add_product as i32,
148+
};
149+
let instruction = Instruction::new_with_bytes(
150+
self.program_id,
151+
bytes_of(&cmd),
152+
vec![
153+
AccountMeta::new(self.payer.pubkey(), true),
154+
AccountMeta::new(mapping_keypair.pubkey(), true),
155+
AccountMeta::new(product_keypair.pubkey(), true),
156+
],
157+
);
158+
159+
self.process_ix(instruction, &vec![&mapping_keypair, &product_keypair])
160+
.await
161+
.map(|_| product_keypair)
162+
}
163+
164+
/// Initialize a price account and add it to an existing product account (using the add_price
165+
/// instruction). Returns the keypair associated with the newly-created account.
166+
pub async fn add_price(
167+
&mut self,
168+
product_keypair: &Keypair,
169+
expo: i32,
170+
) -> Result<Keypair, BanksClientError> {
171+
let price_keypair = self.create_pyth_account(size_of::<pc_price_t>()).await;
172+
173+
let cmd = cmd_add_price_t {
174+
ver_: PC_VERSION,
175+
cmd_: command_t_e_cmd_add_price as i32,
176+
expo_: expo,
177+
ptype_: PC_PTYPE_PRICE,
178+
};
179+
let instruction = Instruction::new_with_bytes(
180+
self.program_id,
181+
bytes_of(&cmd),
182+
vec![
183+
AccountMeta::new(self.payer.pubkey(), true),
184+
AccountMeta::new(product_keypair.pubkey(), true),
185+
AccountMeta::new(price_keypair.pubkey(), true),
186+
],
187+
);
188+
189+
self.process_ix(instruction, &vec![&product_keypair, &price_keypair])
190+
.await
191+
.map(|_| price_keypair)
192+
}
193+
194+
/// Delete a price account from an existing product account (using the del_price instruction).
195+
pub async fn del_price(
196+
&mut self,
197+
product_keypair: &Keypair,
198+
price_keypair: &Keypair,
199+
) -> Result<(), BanksClientError> {
200+
let cmd = cmd_hdr_t {
201+
ver_: PC_VERSION,
202+
cmd_: command_t_e_cmd_del_price as i32,
203+
};
204+
let instruction = Instruction::new_with_bytes(
205+
self.program_id,
206+
bytes_of(&cmd),
207+
vec![
208+
AccountMeta::new(self.payer.pubkey(), true),
209+
AccountMeta::new(product_keypair.pubkey(), true),
210+
AccountMeta::new(price_keypair.pubkey(), true),
211+
],
212+
);
213+
214+
self.process_ix(instruction, &vec![&product_keypair, &price_keypair])
215+
.await
216+
}
217+
218+
/// Get the account at `key`. Returns `None` if no such account exists.
219+
pub async fn get_account(&mut self, key: Pubkey) -> Option<Account> {
220+
self.banks_client.get_account(key).await.unwrap()
221+
}
222+
223+
/// Get the content of an account as a value of type `T`. This function returns a copy of the
224+
/// account data -- you cannot mutate the result to mutate the on-chain account data.
225+
/// Returns None if the account does not exist. Panics if the account data cannot be read as a
226+
/// `T`.
227+
pub async fn get_account_data_as<T: Pod>(&mut self, key: Pubkey) -> Option<T> {
228+
self.get_account(key)
229+
.await
230+
.map(|x| load::<T>(&x.data).unwrap().clone())
231+
}
232+
}

0 commit comments

Comments
 (0)