Skip to content

Commit d33e649

Browse files
authoredSep 6, 2024··
Man0s/crossbar legacy indexer (#245)
* cli: lut check cmd * indexer: swb pull support * fix: exclude indexer from workspace
1 parent 24dfeb1 commit d33e649

File tree

12 files changed

+464
-40
lines changed

12 files changed

+464
-40
lines changed
 

‎clients/rust/marginfi-cli/src/entrypoint.rs

+12
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ pub enum GroupCommand {
153153
#[clap(short = 't', long)]
154154
existing_token_lookup_tables: Vec<Pubkey>,
155155
},
156+
CheckLookupTable {
157+
#[clap(short = 't', long)]
158+
existing_token_lookup_tables: Vec<Pubkey>,
159+
},
156160
}
157161

158162
#[derive(Clone, Copy, Debug, Parser, ArgEnum)]
@@ -606,6 +610,14 @@ fn group(subcmd: GroupCommand, global_options: &GlobalOptions) -> Result<()> {
606610
processor::handle_bankruptcy_for_accounts(&config, &profile, accounts)
607611
}
608612

613+
GroupCommand::CheckLookupTable {
614+
existing_token_lookup_tables,
615+
} => processor::group::process_check_lookup_tables(
616+
&config,
617+
&profile,
618+
existing_token_lookup_tables,
619+
),
620+
609621
GroupCommand::UpdateLookupTable {
610622
existing_token_lookup_tables,
611623
} => processor::group::process_update_lookup_tables(

‎clients/rust/marginfi-cli/src/processor/group.rs

+121
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,127 @@ use std::mem::size_of;
1515
const CHUNK_SIZE: usize = 22;
1616
const KEY_BATCH_SIZE: usize = 20;
1717

18+
pub fn process_check_lookup_tables(
19+
config: &Config,
20+
profile: &Profile,
21+
existing_lookup_tables: Vec<Pubkey>,
22+
) -> Result<()> {
23+
let rpc = config.mfi_program.rpc();
24+
let marginfi_group = profile.marginfi_group.expect("group not set");
25+
26+
let mut accounts: Vec<Account> = vec![];
27+
28+
for chunk in existing_lookup_tables.chunks(CHUNK_SIZE) {
29+
let accounts_2: Vec<Account> = rpc
30+
.get_multiple_accounts(chunk)?
31+
.into_iter()
32+
.flatten()
33+
.collect();
34+
35+
accounts.extend(accounts_2);
36+
}
37+
38+
let lookup_tables: Vec<AddressLookupTable> = accounts
39+
.iter_mut()
40+
.zip(existing_lookup_tables.iter())
41+
.map(|(account, address)| {
42+
let lookup_table = AddressLookupTable::deserialize(&account.data).unwrap();
43+
println!(
44+
"Loaded table {} with {} addresses",
45+
address,
46+
lookup_table.addresses.len()
47+
);
48+
49+
if lookup_table.meta.authority != Some(config.authority()) {
50+
println!(
51+
"Lookup table {} has wrong authority {:?}",
52+
address, lookup_table.meta.authority,
53+
);
54+
}
55+
56+
lookup_table
57+
})
58+
.collect();
59+
60+
let banks = config
61+
.mfi_program
62+
.accounts::<Bank>(vec![RpcFilterType::Memcmp(Memcmp::new_raw_bytes(
63+
8 + size_of::<Pubkey>() + size_of::<u8>(),
64+
marginfi_group.to_bytes().to_vec(),
65+
))])?;
66+
67+
let _bank_pks = banks.iter().map(|(pk, _)| *pk).collect::<Vec<Pubkey>>();
68+
69+
let oracle_pks = banks
70+
.iter()
71+
.flat_map(|(_, bank)| bank.config.oracle_keys)
72+
.filter(|pk| pk != &Pubkey::default())
73+
.collect::<Vec<Pubkey>>();
74+
75+
// Dedup the oracle pks.
76+
let _oracle_pks = oracle_pks
77+
.into_iter()
78+
.fold(vec![], |mut acc, pk| {
79+
if !acc.contains(&pk) {
80+
acc.push(pk);
81+
}
82+
acc
83+
})
84+
.into_iter()
85+
.collect::<Vec<Pubkey>>();
86+
87+
// Join keys
88+
let mut keys = vec![
89+
config.mfi_program.id(),
90+
marginfi_group,
91+
spl_token::id(),
92+
system_program::id(),
93+
];
94+
95+
for (bank_pk, bank) in banks.iter() {
96+
keys.push(*bank_pk);
97+
keys.push(bank.liquidity_vault);
98+
let (vault_auth, _) = utils::find_bank_vault_authority_pda(
99+
bank_pk,
100+
marginfi::state::marginfi_group::BankVaultType::Liquidity,
101+
&marginfi::ID,
102+
);
103+
104+
keys.push(vault_auth);
105+
106+
keys.extend_from_slice(
107+
&bank
108+
.config
109+
.oracle_keys
110+
.iter()
111+
.filter(|pk| **pk != Pubkey::default())
112+
.cloned()
113+
.collect::<Vec<_>>(),
114+
);
115+
}
116+
117+
keys.dedup();
118+
119+
// Find missing keys in lookup tables
120+
let missing_keys = keys
121+
.iter()
122+
.filter(|pk| {
123+
let missing = !lookup_tables
124+
.iter()
125+
.any(|lookup_table| lookup_table.addresses.iter().any(|address| &address == pk));
126+
127+
println!("Key {} missing: {}", pk, missing);
128+
129+
missing
130+
})
131+
.cloned()
132+
.collect::<Vec<Pubkey>>();
133+
134+
println!("Missing {} keys", missing_keys.len());
135+
136+
Ok(())
137+
}
138+
18139
pub fn process_update_lookup_tables(
19140
config: &Config,
20141
profile: &Profile,

‎observability/indexer/Cargo.toml

+12-4
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,22 @@ marginfi = { path = "../../programs/marginfi", features = [
2828
] }
2929

3030
gcp-bigquery-client = "0.16.7"
31-
google-cloud-default = { git = " https://github.com/mrgnlabs/google-cloud-rust.git", rev = "3f651f2d9fd8cca547bb11490d2575d9bf90f994", features = ["pubsub"] }
31+
google-cloud-default = { git = " https://github.com/mrgnlabs/google-cloud-rust.git", rev = "3f651f2d9fd8cca547bb11490d2575d9bf90f994", features = [
32+
"pubsub",
33+
] }
3234
google-cloud-auth = { git = " https://github.com/mrgnlabs/google-cloud-rust.git", rev = "3f651f2d9fd8cca547bb11490d2575d9bf90f994" }
3335
google-cloud-pubsub = { git = " https://github.com/mrgnlabs/google-cloud-rust.git", rev = "3f651f2d9fd8cca547bb11490d2575d9bf90f994" }
3436
google-cloud-gax = { git = " https://github.com/mrgnlabs/google-cloud-rust.git", rev = "3f651f2d9fd8cca547bb11490d2575d9bf90f994" }
35-
google-cloud-googleapis = { git = " https://github.com/mrgnlabs/google-cloud-rust.git", rev = "3f651f2d9fd8cca547bb11490d2575d9bf90f994", features = ["bytes", "pubsub"] }
37+
google-cloud-googleapis = { git = " https://github.com/mrgnlabs/google-cloud-rust.git", rev = "3f651f2d9fd8cca547bb11490d2575d9bf90f994", features = [
38+
"bytes",
39+
"pubsub",
40+
] }
3641
yup-oauth2 = "8.3.0"
37-
yellowstone-grpc-client = { git = "https://github.com/rpcpool/yellowstone-grpc.git", rev = "a2cd1498ac64baa1017d4a4cdefbf46100215b4c" }
38-
yellowstone-grpc-proto = { git = "https://github.com/rpcpool/yellowstone-grpc.git", rev = "a2cd1498ac64baa1017d4a4cdefbf46100215b4c" }
42+
yellowstone-grpc-client = { git = "https://github.com/rpcpool/yellowstone-grpc.git", rev = "87e1755b0d7a4e8101cb5feb6f30063aa91f343f" }
43+
yellowstone-grpc-proto = { git = "https://github.com/rpcpool/yellowstone-grpc.git", rev = "87e1755b0d7a4e8101cb5feb6f30063aa91f343f" }
44+
switchboard-on-demand-client = "0.1.7"
45+
switchboard-on-demand = "0.1.7"
46+
hex = "0.4.3"
3947
fixed = "1.12.0"
4048
fixed-macro = "1.2.0"
4149
dotenv = "0.15.0"

‎observability/indexer/src/commands/index_accounts.rs

+7-5
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,13 @@ pub async fn index_accounts(config: IndexAccountsConfig) -> Result<()> {
112112
async fn listen_to_updates(ctx: Arc<Context>) {
113113
loop {
114114
info!("Connecting geyser client");
115-
let geyser_client_connection_result = GeyserGrpcClient::connect(
116-
ctx.config.rpc_endpoint.to_string(),
117-
Some(ctx.config.rpc_token.to_string()),
118-
None,
119-
);
115+
let geyser_client_connection_result =
116+
GeyserGrpcClient::build_from_shared(ctx.config.rpc_endpoint.to_string())
117+
.unwrap()
118+
.x_token(Some(ctx.config.rpc_token.to_string()))
119+
.unwrap()
120+
.connect()
121+
.await;
120122

121123
let mut geyser_client = match geyser_client_connection_result {
122124
Ok(geyser_client) => geyser_client,

‎observability/indexer/src/commands/index_transactions.rs

+7-10
Original file line numberDiff line numberDiff line change
@@ -115,16 +115,13 @@ pub async fn index_transactions(config: IndexTransactionsConfig) -> Result<()> {
115115
async fn listen_to_updates(ctx: Arc<Context>) {
116116
loop {
117117
info!("Connecting geyser client");
118-
let geyser_client_connection_result = GeyserGrpcClient::connect_with_timeout(
119-
ctx.config.rpc_endpoint.to_string(),
120-
Some(ctx.config.rpc_token.to_string()),
121-
None,
122-
Some(Duration::from_secs(10)),
123-
Some(Duration::from_secs(10)),
124-
false,
125-
)
126-
.await;
127-
info!("Connected");
118+
let geyser_client_connection_result =
119+
GeyserGrpcClient::build_from_shared(ctx.config.rpc_endpoint.to_string())
120+
.unwrap()
121+
.x_token(Some(ctx.config.rpc_token.to_string()))
122+
.unwrap()
123+
.connect()
124+
.await;
128125

129126
let mut geyser_client = match geyser_client_connection_result {
130127
Ok(geyser_client) => geyser_client,

‎observability/indexer/src/commands/snapshot_accounts.rs

+56-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use crate::utils::convert_account;
2+
use crate::utils::crossbar::{CrossbarCache, SwbPullFeedMeta};
23
use crate::utils::metrics::{LendingPoolBankMetrics, MarginfiAccountMetrics, MarginfiGroupMetrics};
3-
use crate::utils::snapshot::Snapshot;
44
use crate::utils::snapshot::{AccountRoutingType, BankUpdateRoutingType};
5+
use crate::utils::snapshot::{OracleData, Snapshot};
6+
use crate::utils::swb_pull::overwrite_price_from_sim;
57
use anyhow::Result;
68
use chrono::{DateTime, Utc};
79
use envconfig::Envconfig;
@@ -112,14 +114,16 @@ pub struct Context {
112114
account_updates_queue: Arc<Mutex<BTreeMap<u64, HashMap<Pubkey, AccountUpdate>>>>,
113115
latest_slots_with_commitment: Arc<Mutex<BTreeSet<u64>>>,
114116
account_snapshot: Arc<Mutex<Snapshot>>,
117+
crossbar_store: Arc<CrossbarCache>,
115118
stream_disconnection_count: Arc<AtomicU64>,
116119
update_processing_error_count: Arc<AtomicU64>,
117120
}
118121

119122
impl Context {
120123
pub async fn new(config: &SnapshotAccountsConfig) -> Self {
124+
let rpc_endpoint = format!("{}/{}", config.rpc_endpoint, config.rpc_token);
121125
let rpc_client = Arc::new(RpcClient::new_with_commitment(
122-
format!("{}/{}", config.rpc_endpoint, config.rpc_token),
126+
rpc_endpoint,
123127
CommitmentConfig {
124128
commitment: solana_sdk::commitment_config::CommitmentLevel::Finalized,
125129
},
@@ -132,6 +136,7 @@ impl Context {
132136
account_updates_queue: Arc::new(Mutex::new(BTreeMap::new())),
133137
latest_slots_with_commitment: Arc::new(Mutex::new(BTreeSet::new())),
134138
account_snapshot: Arc::new(Mutex::new(Snapshot::new(config.program_id, rpc_client))),
139+
crossbar_store: Arc::new(CrossbarCache::new()),
135140
stream_disconnection_count: Arc::new(AtomicU64::new(0)),
136141
update_processing_error_count: Arc::new(AtomicU64::new(0)),
137142
}
@@ -188,6 +193,26 @@ pub async fn snapshot_accounts(config: SnapshotAccountsConfig) -> Result<()> {
188193
snapshot.init().await.unwrap();
189194
info!("Summary: {snapshot}");
190195

196+
let swb_feed_accounts_and_hashes = snapshot
197+
.price_feeds
198+
.iter()
199+
.filter_map(|(pk, od)| match od {
200+
OracleData::SwitchboardPull(feed) => Some((*pk, hex::encode(feed.feed.feed_hash))),
201+
_ => None,
202+
})
203+
.collect::<Vec<_>>();
204+
205+
context.crossbar_store.track_feeds(
206+
swb_feed_accounts_and_hashes
207+
.into_iter()
208+
.map(|(feed_address, feed_hash)| SwbPullFeedMeta {
209+
feed_hash,
210+
feed_address,
211+
})
212+
.collect::<Vec<_>>(),
213+
);
214+
context.crossbar_store.refresh_prices().await;
215+
191216
snapshot
192217
.routing_lookup
193218
.iter()
@@ -207,6 +232,26 @@ pub async fn snapshot_accounts(config: SnapshotAccountsConfig) -> Result<()> {
207232
let geyser_subscription_config = compute_geyser_config(&config, &non_program_accounts).await;
208233
*context.geyser_subscription_config.lock().await = (false, geyser_subscription_config.clone());
209234

235+
let update_crossbar_cache_handle = tokio::spawn({
236+
let context = context.clone();
237+
async move {
238+
loop {
239+
context.crossbar_store.refresh_prices().await;
240+
let mut snapshot = context.account_snapshot.lock().await;
241+
let feeds_per_address: HashMap<Pubkey, crate::utils::crossbar::SimulatedPrice> =
242+
context.crossbar_store.get_prices_per_address();
243+
for (address, price) in feeds_per_address {
244+
if let Some(od) = snapshot.price_feeds.get_mut(&address) {
245+
if let OracleData::SwitchboardPull(feed) = od {
246+
overwrite_price_from_sim(feed, &price);
247+
}
248+
}
249+
}
250+
tokio::time::sleep(std::time::Duration::from_secs(20)).await;
251+
}
252+
}
253+
});
254+
210255
let listen_to_updates_handle = tokio::spawn({
211256
let context = context.clone();
212257
async move { listen_to_updates(context).await }
@@ -226,6 +271,7 @@ pub async fn snapshot_accounts(config: SnapshotAccountsConfig) -> Result<()> {
226271
});
227272

228273
join_all([
274+
update_crossbar_cache_handle,
229275
listen_to_updates_handle,
230276
process_account_updates_handle,
231277
update_account_map_handle,
@@ -239,15 +285,14 @@ pub async fn snapshot_accounts(config: SnapshotAccountsConfig) -> Result<()> {
239285
async fn listen_to_updates(ctx: Arc<Context>) {
240286
loop {
241287
info!("Connecting geyser client");
242-
let geyser_client_connection_result = GeyserGrpcClient::connect_with_timeout(
243-
ctx.config.rpc_endpoint.to_string(),
244-
Some(ctx.config.rpc_token.to_string()),
245-
None,
246-
Some(Duration::from_secs(10)),
247-
Some(Duration::from_secs(10)),
248-
false,
249-
)
250-
.await;
288+
let geyser_client_connection_result =
289+
GeyserGrpcClient::build_from_shared(ctx.config.rpc_endpoint.to_string())
290+
.unwrap()
291+
.x_token(Some(ctx.config.rpc_token.to_string()))
292+
.unwrap()
293+
.connect()
294+
.await;
295+
251296
info!("Connected");
252297

253298
let mut geyser_client = match geyser_client_connection_result {
+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
use solana_sdk::pubkey::Pubkey;
2+
use std::{collections::HashMap, sync::Mutex};
3+
use switchboard_on_demand_client::CrossbarClient;
4+
5+
pub struct SwbPullFeedMeta {
6+
pub feed_address: Pubkey,
7+
pub feed_hash: String,
8+
}
9+
10+
pub struct SwbPullFeedInfo {
11+
pub feed_meta: SwbPullFeedMeta,
12+
pub simulated_price: SimulatedPrice,
13+
}
14+
15+
#[derive(Clone, Debug)]
16+
pub struct SimulatedPrice {
17+
pub value: f64,
18+
pub std_dev: f64,
19+
pub timestamp: i64,
20+
}
21+
22+
pub struct CrossbarCache {
23+
crossbar_client: CrossbarClient,
24+
pub feeds: Mutex<HashMap<String, SwbPullFeedInfo>>,
25+
}
26+
27+
impl CrossbarCache {
28+
/// Creates a new CrossbarCache empty instance
29+
pub fn new() -> Self {
30+
let crossbar_client = CrossbarClient::default(None);
31+
Self {
32+
crossbar_client,
33+
feeds: Mutex::new(HashMap::new()),
34+
}
35+
}
36+
37+
pub fn track_feeds(&self, feeds: Vec<SwbPullFeedMeta>) {
38+
for feed in feeds.into_iter() {
39+
self.feeds.lock().unwrap().insert(
40+
feed.feed_hash.clone(),
41+
SwbPullFeedInfo {
42+
feed_meta: feed,
43+
simulated_price: SimulatedPrice {
44+
value: 0.0,
45+
std_dev: 0.0,
46+
timestamp: 0,
47+
},
48+
},
49+
);
50+
}
51+
}
52+
53+
pub async fn refresh_prices(&self) {
54+
if self.feeds.lock().unwrap().is_empty() {
55+
return;
56+
}
57+
58+
let feed_hashes = self
59+
.feeds
60+
.lock()
61+
.unwrap()
62+
.values()
63+
.map(|feed| feed.feed_meta.feed_hash.clone())
64+
.collect::<Vec<_>>();
65+
66+
let simulated_prices = self
67+
.crossbar_client
68+
.simulate_feeds(&feed_hashes.iter().map(|x| x.as_str()).collect::<Vec<_>>())
69+
.await
70+
.unwrap();
71+
72+
let timestamp = chrono::Utc::now().timestamp();
73+
74+
let mut feeds = self.feeds.lock().unwrap();
75+
for simulated_response in simulated_prices {
76+
if let Some(price) = calculate_price(simulated_response.results) {
77+
if let Some(feed) = feeds.get_mut(&simulated_response.feedHash) {
78+
feed.simulated_price = SimulatedPrice {
79+
value: price,
80+
std_dev: 0.0,
81+
timestamp,
82+
};
83+
}
84+
}
85+
}
86+
}
87+
88+
pub fn get_prices_per_address(&self) -> HashMap<Pubkey, SimulatedPrice> {
89+
let mut feeds_per_address = HashMap::new();
90+
let feeds = self.feeds.lock().unwrap();
91+
for feed in feeds.values() {
92+
feeds_per_address.insert(feed.feed_meta.feed_address, feed.simulated_price.clone());
93+
}
94+
feeds_per_address
95+
}
96+
}
97+
98+
/// Calculate the median of a list of numbers
99+
fn calculate_price(mut numbers: Vec<f64>) -> Option<f64> {
100+
if numbers.is_empty() {
101+
return None;
102+
}
103+
104+
numbers.sort_by(|a, b| a.partial_cmp(b).unwrap());
105+
let mid = numbers.len() / 2;
106+
107+
if numbers.len() % 2 == 0 {
108+
Some((numbers[mid - 1] + numbers[mid]) / 2.0)
109+
} else {
110+
Some(numbers[mid])
111+
}
112+
}
113+
114+
#[cfg(test)]
115+
mod tests {
116+
use super::*;
117+
use std::sync::Arc;
118+
use std::sync::Mutex;
119+
120+
#[tokio::test]
121+
async fn test_crossbar_maintainer_new() {
122+
let price = Arc::new(Mutex::new(0.0));
123+
let feed_hash =
124+
"0x4c935636f2523f6aeeb6dc7b7dab0e86a13ff2c794f7895fc78851d69fdb593b".to_string();
125+
let price2 = Arc::new(Mutex::new(0.0));
126+
let feed_hash2 =
127+
"0x5686ebe26b52d5c67dc10b63240c6d937af75d86bfcacf46865cd5da62f760e9".to_string();
128+
let crossbar_maintainer = CrossbarCache::new();
129+
crossbar_maintainer.track_feeds(vec![
130+
SwbPullFeedMeta {
131+
feed_address: Pubkey::new_unique(),
132+
feed_hash: feed_hash.clone(),
133+
},
134+
SwbPullFeedMeta {
135+
feed_address: Pubkey::new_unique(),
136+
feed_hash: feed_hash2.clone(),
137+
},
138+
]);
139+
crossbar_maintainer.refresh_prices().await;
140+
println!("Price: {:?}", price.lock().unwrap());
141+
println!("Price2: {:?}", price2.lock().unwrap());
142+
}
143+
}

‎observability/indexer/src/utils/metrics.rs

+9-4
Original file line numberDiff line numberDiff line change
@@ -464,15 +464,20 @@ impl MarginfiAccountMetrics {
464464
match oracle_data {
465465
OracleData::Pyth(price_feed) => (
466466
*oracle_pk,
467-
OraclePriceFeedAdapter::PythEma(price_feed.clone()),
467+
OraclePriceFeedAdapter::PythLegacy(price_feed.clone()),
468468
),
469469
OracleData::Switchboard(pf) => (
470470
*oracle_pk,
471471
OraclePriceFeedAdapter::SwitchboardV2(pf.clone()),
472472
),
473-
OracleData::PythPush(pf) => {
474-
(*oracle_pk, OraclePriceFeedAdapter::PythPush(pf.clone()))
475-
}
473+
OracleData::PythPush(pf) => (
474+
*oracle_pk,
475+
OraclePriceFeedAdapter::PythPushOracle(pf.clone()),
476+
),
477+
OracleData::SwitchboardPull(pf) => (
478+
*oracle_pk,
479+
OraclePriceFeedAdapter::SwitchboardPull(pf.clone()),
480+
),
476481
}
477482
}));
478483

‎observability/indexer/src/utils/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use solana_sdk::{account::Account, pubkey::Pubkey};
22

33
pub mod big_query;
4+
pub mod crossbar;
45
pub mod errors;
56
pub mod marginfi_account_dup;
67
pub mod metrics;
78
pub mod protos;
89
pub mod snapshot;
10+
pub mod swb_pull;
911
pub mod transactions_crawler;
1012

1113
pub fn convert_account(

‎observability/indexer/src/utils/snapshot.rs

+30-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::common::get_multiple_accounts_chunked2;
12
use anchor_client::anchor_lang::AccountDeserialize;
23
use anchor_client::anchor_lang::Discriminator;
34
use fixed::types::I80F48;
@@ -24,7 +25,7 @@ use std::{
2425
};
2526
use tracing::info;
2627

27-
use crate::common::get_multiple_accounts_chunked2;
28+
use super::swb_pull::load_swb_pull_account;
2829

2930
#[derive(Clone, Debug)]
3031
pub struct BankAccounts {
@@ -41,6 +42,7 @@ pub enum AccountRoutingType {
4142
Bank(Pubkey, BankUpdateRoutingType),
4243
PriceFeedPyth,
4344
PriceFeedSwitchboard,
45+
PriceFeedSwitchboardPull,
4446
PriceFeedPythPushOracle,
4547
}
4648

@@ -54,9 +56,10 @@ pub enum BankUpdateRoutingType {
5456

5557
#[derive(Clone, Debug)]
5658
pub enum OracleData {
57-
Pyth(PythEmaPriceFeed),
59+
Pyth(PythLegacyPriceFeed),
5860
Switchboard(SwitchboardV2PriceFeed),
5961
PythPush(PythPushOraclePriceFeed),
62+
SwitchboardPull(SwitchboardPullPriceFeed),
6063
}
6164

6265
impl OracleData {
@@ -75,6 +78,9 @@ impl OracleData {
7578
OracleData::PythPush(price_feed) => price_feed
7679
.get_price_of_type(oracle_price_type, bias)
7780
.unwrap(),
81+
OracleData::SwitchboardPull(price_feed) => price_feed
82+
.get_price_of_type(oracle_price_type, bias)
83+
.unwrap(),
7884
}
7985
}
8086
}
@@ -219,7 +225,7 @@ impl Snapshot {
219225

220226
match bank.config.oracle_setup {
221227
OracleSetup::None => (),
222-
OracleSetup::PythEma => {
228+
OracleSetup::PythLegacy => {
223229
let oracle_address = bank.config.oracle_keys[0];
224230
self.routing_lookup
225231
.insert(oracle_address, AccountRoutingType::PriceFeedPyth);
@@ -252,6 +258,15 @@ impl Snapshot {
252258
mfi_sponsored_oracle_address,
253259
AccountRoutingType::PriceFeedPythPushOracle,
254260
);
261+
262+
accounts_to_fetch.push(pyth_sponsored_oracle_address);
263+
accounts_to_fetch.push(mfi_sponsored_oracle_address);
264+
}
265+
OracleSetup::SwitchboardPull => {
266+
let oracle_address = bank.config.oracle_keys[0];
267+
self.routing_lookup
268+
.insert(oracle_address, AccountRoutingType::PriceFeedSwitchboardPull);
269+
accounts_to_fetch.push(oracle_address);
255270
}
256271
}
257272

@@ -307,7 +322,7 @@ impl Snapshot {
307322
AccountRoutingType::PriceFeedPyth => {
308323
let mut account = account.clone();
309324
let ai = (account_pubkey, &mut account).into_account_info();
310-
let pf = PythEmaPriceFeed::load_checked(&ai, 0, u64::MAX).unwrap();
325+
let pf = PythLegacyPriceFeed::load_checked(&ai, 0, u64::MAX).unwrap();
311326
self.price_feeds
312327
.insert(*account_pubkey, OracleData::Pyth(pf));
313328
}
@@ -362,6 +377,17 @@ impl Snapshot {
362377
self.price_feeds
363378
.insert(feed_id_pk, OracleData::PythPush(pf));
364379
}
380+
AccountRoutingType::PriceFeedSwitchboardPull => {
381+
let mut account = account.clone();
382+
let ai = (account_pubkey, &mut account).into_account_info();
383+
let pf = load_swb_pull_account(&ai).unwrap();
384+
self.price_feeds.insert(
385+
*account_pubkey,
386+
OracleData::SwitchboardPull(SwitchboardPullPriceFeed {
387+
feed: Box::new((&pf).into()),
388+
}),
389+
);
390+
}
365391
}
366392
}
367393
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use fixed::types::I80F48;
2+
use marginfi::constants::EXP_10_I80F48;
3+
use marginfi::state::price::SwitchboardPullPriceFeed;
4+
use solana_sdk::account_info::AccountInfo;
5+
use switchboard_on_demand::PullFeedAccountData;
6+
7+
use super::crossbar::SimulatedPrice;
8+
9+
pub fn overwrite_price_from_sim(
10+
current_data: &mut SwitchboardPullPriceFeed,
11+
simulated_price: &SimulatedPrice,
12+
) {
13+
let value: i128 = I80F48::from_num(simulated_price.value)
14+
.checked_mul(EXP_10_I80F48[switchboard_on_demand::PRECISION as usize])
15+
.unwrap()
16+
.to_num();
17+
let std_dev: i128 = I80F48::from_num(simulated_price.std_dev)
18+
.checked_mul(EXP_10_I80F48[switchboard_on_demand::PRECISION as usize])
19+
.unwrap()
20+
.to_num();
21+
22+
current_data.feed.result.value = value;
23+
current_data.feed.result.std_dev = std_dev;
24+
// other fields are ignored because not used by the indexer
25+
}
26+
27+
pub fn load_swb_pull_account(account_info: &AccountInfo) -> anyhow::Result<PullFeedAccountData> {
28+
let bytes = &account_info.data.borrow().to_vec();
29+
30+
if bytes
31+
.as_ptr()
32+
.align_offset(std::mem::align_of::<PullFeedAccountData>())
33+
!= 0
34+
{
35+
return Err(anyhow::anyhow!("Invalid alignment"));
36+
}
37+
38+
let num = bytes.len() / std::mem::size_of::<PullFeedAccountData>();
39+
let mut vec: Vec<PullFeedAccountData> = Vec::with_capacity(num);
40+
41+
unsafe {
42+
vec.set_len(num);
43+
std::ptr::copy_nonoverlapping(
44+
bytes[8..std::mem::size_of::<PullFeedAccountData>() + 8].as_ptr(),
45+
vec.as_mut_ptr() as *mut u8,
46+
bytes.len(),
47+
);
48+
}
49+
50+
Ok(vec[0])
51+
}

‎programs/marginfi/src/state/price.rs

+14-2
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ impl PriceAdapter for PythLegacyPriceFeed {
308308

309309
#[cfg_attr(feature = "client", derive(Clone, Debug))]
310310
pub struct SwitchboardPullPriceFeed {
311-
feed: Box<LitePullFeedAccountData>,
311+
pub feed: Box<LitePullFeedAccountData>,
312312
}
313313

314314
impl SwitchboardPullPriceFeed {
@@ -767,14 +767,22 @@ impl PriceAdapter for PythPushOraclePriceFeed {
767767
/// A slimmed down version of the PullFeedAccountData struct copied from the
768768
/// switchboard-on-demand/src/pull_feed.rs
769769
#[cfg_attr(feature = "client", derive(Clone, Debug))]
770-
struct LitePullFeedAccountData {
770+
pub struct LitePullFeedAccountData {
771771
pub result: CurrentResult,
772+
#[cfg(feature = "client")]
773+
pub feed_hash: [u8; 32],
774+
#[cfg(feature = "client")]
775+
pub last_update_timestamp: i64,
772776
}
773777

774778
impl From<&PullFeedAccountData> for LitePullFeedAccountData {
775779
fn from(feed: &PullFeedAccountData) -> Self {
776780
Self {
777781
result: feed.result,
782+
#[cfg(feature = "client")]
783+
feed_hash: feed.feed_hash,
784+
#[cfg(feature = "client")]
785+
last_update_timestamp: feed.last_update_timestamp,
778786
}
779787
}
780788
}
@@ -783,6 +791,10 @@ impl From<Ref<'_, PullFeedAccountData>> for LitePullFeedAccountData {
783791
fn from(feed: Ref<'_, PullFeedAccountData>) -> Self {
784792
Self {
785793
result: feed.result,
794+
#[cfg(feature = "client")]
795+
feed_hash: feed.feed_hash,
796+
#[cfg(feature = "client")]
797+
last_update_timestamp: feed.last_update_timestamp,
786798
}
787799
}
788800
}

0 commit comments

Comments
 (0)
Please sign in to comment.