Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: wrap Pythnet/Solana accounts generically
Browse files Browse the repository at this point in the history
Reisen committed Feb 12, 2024
1 parent 2fa7af1 commit 23a0193
Showing 5 changed files with 1,819 additions and 749 deletions.
2,416 changes: 1,702 additions & 714 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -26,11 +26,11 @@ serde_json = "1.0.79"
chrono = "0.4.31"
chrono-tz = "0.8.4"
parking_lot = "0.12.1"
pyth-sdk = "0.7.0"
pyth-sdk-solana = "0.7.2"
solana-account-decoder = "1.14.16"
solana-client = "1.14.16"
solana-sdk = "1.14.16"
pyth-sdk = "0.8.0"
pyth-sdk-solana = "0.10.0"
solana-account-decoder = "1.17.20"
solana-client = "1.17.20"
solana-sdk = "1.17.20"
bincode = "1.3.3"
slog = { version = "2.7.0", features = ["max_level_trace", "release_max_level_trace"] }
slog-term = "2.9.0"
47 changes: 31 additions & 16 deletions src/agent/pythd/adapter.rs
Original file line number Diff line number Diff line change
@@ -21,7 +21,10 @@ use {
SubscriptionID,
},
},
crate::agent::store::global::AllAccountsData,
crate::agent::{
solana::oracle::PriceEntry,
store::global::AllAccountsData,
},
anyhow::{
anyhow,
Result,
@@ -404,7 +407,7 @@ impl Adapter {

fn solana_price_account_to_pythd_api_price_account(
price_account_key: &solana_sdk::pubkey::Pubkey,
price_account: &pyth_sdk_solana::state::PriceAccount,
price_account: &PriceEntry,
) -> api::PriceAccount {
api::PriceAccount {
account: price_account_key.to_string(),
@@ -423,7 +426,7 @@ impl Adapter {
publisher_accounts: price_account
.comp
.iter()
.filter(|comp| **comp != PriceComp::default())
.filter(|comp| *comp != &PriceComp::default())
.map(|comp| api::PublisherAccount {
account: comp.publisher.to_string(),
status: Self::price_status_to_str(comp.agg.status),
@@ -630,12 +633,12 @@ mod tests {
iobuffer::IoBuffer,
pyth_sdk::Identifier,
pyth_sdk_solana::state::{
PriceAccount,
PriceComp,
PriceInfo,
PriceStatus,
PriceType,
Rational,
SolanaPriceAccount,
},
slog_extlog::slog_test,
std::{
@@ -1076,7 +1079,7 @@ mod tests {
"GVXRSBjFk6e6J3NbVPXohDJetcTjaeeuykUpbQF8UoMU",
)
.unwrap(),
PriceAccount {
SolanaPriceAccount {
magic: 0xa1b2c3d4,
ver: 7,
atype: 9,
@@ -1122,14 +1125,16 @@ mod tests {
pub_slot: 7262746,
},
comp: [PriceComp::default(); 32],
},
extended: (),
}
.into(),
),
(
solana_sdk::pubkey::Pubkey::from_str(
"3VQwtcntVQN1mj1MybQw8qK7Li3KNrrgNskSQwZAPGNr",
)
.unwrap(),
PriceAccount {
SolanaPriceAccount {
magic: 0xa1b2c3d4,
ver: 6,
atype: 4,
@@ -1194,14 +1199,16 @@ mod tests {
pub_slot: 4368,
},
}]),
},
extended: (),
}
.into(),
),
(
solana_sdk::pubkey::Pubkey::from_str(
"2V7t5NaKY7aGkwytCWQgvUYZfEr9XMwNChhJEakTExk6",
)
.unwrap(),
PriceAccount {
SolanaPriceAccount {
magic: 0xa1b2c3d4,
ver: 7,
atype: 6,
@@ -1288,14 +1295,16 @@ mod tests {
},
},
]),
},
extended: (),
}
.into(),
),
(
solana_sdk::pubkey::Pubkey::from_str(
"GG3FTE7xhc9Diy7dn9P6BWzoCrAEE4D3p5NBYrDAm5DD",
)
.unwrap(),
PriceAccount {
SolanaPriceAccount {
magic: 0xa1b2c3d4,
ver: 6,
atype: 6,
@@ -1379,14 +1388,16 @@ mod tests {
},
},
]),
},
extended: (),
}
.into(),
),
(
solana_sdk::pubkey::Pubkey::from_str(
"fTNjSfj5uW9e4CAMHzUcm65ftRNBxCN1gG5GS1mYfid",
)
.unwrap(),
PriceAccount {
SolanaPriceAccount {
magic: 0xa1b2c3d4,
ver: 8,
atype: 4,
@@ -1473,14 +1484,16 @@ mod tests {
},
},
]),
},
extended: (),
}
.into(),
),
(
solana_sdk::pubkey::Pubkey::from_str(
"GKNcUmNacSJo4S2Kq3DuYRYRGw3sNUfJ4tyqd198t6vQ",
)
.unwrap(),
PriceAccount {
SolanaPriceAccount {
magic: 0xa1b2c3d4,
ver: 6,
atype: 3,
@@ -1542,7 +1555,9 @@ mod tests {
pub_slot: 7101326,
},
}]),
},
extended: (),
}
.into(),
),
]),
}
2 changes: 1 addition & 1 deletion src/agent/pythd/api.rs
Original file line number Diff line number Diff line change
@@ -813,7 +813,7 @@ pub mod rpc {

// Create and spawn a server (the SUT)
let (shutdown_tx, shutdown_rx) = broadcast::channel(10);
let mut log_buffer = IoBuffer::new();
let log_buffer = IoBuffer::new();
let logger = slog_test::new_test_logger(log_buffer.clone());
let config = Config {
listen_address: format!("127.0.0.1:{:}", listen_port),
93 changes: 80 additions & 13 deletions src/agent/solana/oracle.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
use crate::agent::market_hours::WeeklySchedule;
// This module is responsible for loading the current state of the
// on-chain Oracle program accounts from Solana.
use {
self::subscriber::Subscriber,
super::key_store::KeyStore,
crate::agent::store::global,
crate::agent::{
market_hours::WeeklySchedule,
store::global,
},
anyhow::{
anyhow,
Context,
Result,
},
pyth_sdk_solana::state::{
load_mapping_account,
load_price_account,
load_product_account,
GenericPriceAccount,
PriceComp,
PythnetPriceAccount,
SolanaPriceAccount,
},
serde::{
Deserialize,
@@ -43,6 +48,66 @@ use {
},
};

/// This shim is used to abstract over SolanaPriceAccount and PythnetPriceAccount so we
/// can iterate over either of these. The API is intended to force users to be aware of
/// the account type they have, and so doesn't provide this abstraction (a good thing)
/// and the agent should implement this in a better way.
///
/// For now, to implement the abstraction in the smallest way possible we use a shim
/// type that uses the size of the accounts to determine the underlying representation
/// and construct the right one regardless of which network we read. This will only work
/// as long as we don't care about any extended fields.
///
/// TODO: Refactor the agent's network handling code.
#[derive(Copy, Clone, Debug)]
pub struct PriceEntry {
// We intentionally act as if we have a truncated account where the underlying memory is unavailable.
account: GenericPriceAccount<0, ()>,
pub comp: [PriceComp; 64],
}

impl From<SolanaPriceAccount> for PriceEntry {
fn from(other: SolanaPriceAccount) -> PriceEntry {
unsafe {
// NOTE: We know the size is 32 because It's a Solana account. This is for tests only.
let comp_mem = std::slice::from_raw_parts(other.comp.as_ptr() as *const PriceComp, 32);
let account =
*(&other as *const SolanaPriceAccount as *const GenericPriceAccount<0, ()>);
let mut comp = [PriceComp::default(); 64];
(&mut comp[0..32]).copy_from_slice(comp_mem);
PriceEntry { account, comp }
}
}
}

impl PriceEntry {
/// Construct the right underlying GenericPriceAccount based on the account size.
fn load_from_account(acc: &[u8]) -> Option<Self> {
unsafe {
let size = match acc.len() {
n if n == std::mem::size_of::<SolanaPriceAccount>() => 32,
n if n == std::mem::size_of::<PythnetPriceAccount>() => 64,
_ => return None,
};

let account = *(acc.as_ptr() as *const GenericPriceAccount<0, ()>);
let comp_mem =
std::slice::from_raw_parts(account.comp.as_ptr() as *const PriceComp, size);
let mut comp = [PriceComp::default(); 64];
(&mut comp[0..size]).copy_from_slice(comp_mem);
Some(Self { account, comp })
}
}
}

/// Implement `Deref` so we can access the underlying account fields.
impl std::ops::Deref for PriceEntry {
type Target = GenericPriceAccount<0, ()>;
fn deref(&self) -> &Self::Target {
&self.account
}
}

#[derive(Default, Debug, Clone)]
pub struct Data {
pub mapping_accounts: HashMap<Pubkey, MappingAccount>,
@@ -75,7 +140,6 @@ pub struct ProductEntry {
pub weekly_schedule: WeeklySchedule,
pub price_accounts: Vec<Pubkey>,
}
pub type PriceEntry = pyth_sdk_solana::state::PriceAccount;

// Oracle is responsible for fetching Solana account data stored in the Pyth on-chain Oracle.
pub struct Oracle {
@@ -288,14 +352,16 @@ impl Oracle {
account_key: &Pubkey,
account: &Account,
) -> Result<()> {
let price_account = *load_price_account(&account.data)
let price_entry = PriceEntry::load_from_account(&account.data)
.with_context(|| format!("load price account {}", account_key))?;

debug!(self.logger, "observed on-chain price account update"; "pubkey" => account_key.to_string(), "price" => price_account.agg.price, "conf" => price_account.agg.conf, "status" => format!("{:?}", price_account.agg.status));
debug!(self.logger, "observed on-chain price account update"; "pubkey" => account_key.to_string(), "price" => price_entry.agg.price, "conf" => price_entry.agg.conf, "status" => format!("{:?}", price_entry.agg.status));

self.data.price_accounts.insert(*account_key, price_account);
self.data
.price_accounts
.insert(*account_key, price_entry.clone());

self.notify_price_account_update(account_key, &price_account)
self.notify_price_account_update(account_key, &price_entry)
.await?;

Ok(())
@@ -337,7 +403,7 @@ impl Oracle {
self.global_store_tx
.send(global::Update::PriceAccountUpdate {
account_key: *account_key,
account: *account,
account: account.clone(),
})
.await
.map_err(|_| anyhow!("failed to notify price account update"))
@@ -593,12 +659,13 @@ impl Poller {
// as todo gets replaced with next_todo.
for (price_key, price_account) in todo.iter().zip(price_accounts) {
if let Some(price_acc) = price_account {
let price = load_price_account(&price_acc.data)
let price = PriceEntry::load_from_account(&price_acc.data)
.context(format!("Could not parse price account at {}", price_key))?;

let next_price = price.next;
if let Some(prod) = product_entries.get_mut(&price.prod) {
prod.price_accounts.push(*price_key);
price_entries.insert(*price_key, *price);
price_entries.insert(*price_key, price);
} else {
warn!(self.logger, "Could not find product entry for price, listed in its prod field, skipping";
"missing_product" => price.prod.to_string(),
@@ -608,8 +675,8 @@ impl Poller {
continue;
}

if price.next != Pubkey::default() {
next_todo.push(price.next);
if next_price != Pubkey::default() {
next_todo.push(next_price);
}
} else {
warn!(self.logger, "Could not look up price account on chain, skipping"; "price_key" => price_key.to_string(),);

0 comments on commit 23a0193

Please sign in to comment.