Skip to content

Commit 96af23e

Browse files
committed
refactor(agent): extract oracle component/service
1 parent 216ab59 commit 96af23e

File tree

11 files changed

+922
-969
lines changed

11 files changed

+922
-969
lines changed

src/agent.rs

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -128,19 +128,45 @@ impl Agent {
128128
// Create the Application State.
129129
let state = Arc::new(state::State::new(self.config.state.clone()).await);
130130

131-
// Spawn the primary network
132-
jhs.extend(network::spawn_network(
133-
self.config.primary_network.clone(),
134-
network::Network::Primary,
135-
state.clone(),
136-
)?);
131+
// Spawn the primary network Oracle.
132+
{
133+
// Publisher permissions updates between oracle and exporter
134+
let (publisher_permissions_tx, publisher_permissions_rx) =
135+
watch::channel(<_>::default());
136+
137+
jhs.push(tokio::spawn(services::oracle(
138+
self.config.primary_network.clone(),
139+
network::Network::Primary,
140+
state.clone(),
141+
publisher_permissions_tx.clone(),
142+
)));
143+
144+
// Spawn the primary network
145+
jhs.extend(network::spawn_network(
146+
self.config.primary_network.clone(),
147+
network::Network::Primary,
148+
state.clone(),
149+
publisher_permissions_rx.clone(),
150+
)?);
151+
}
137152

138-
// Spawn the secondary network, if needed
153+
// Spawn the secondary network Oracle, if needed.
139154
if let Some(config) = &self.config.secondary_network {
155+
let (publisher_permissions_tx, publisher_permissions_rx) =
156+
watch::channel(<_>::default());
157+
158+
jhs.push(tokio::spawn(services::oracle(
159+
config.clone(),
160+
network::Network::Secondary,
161+
state.clone(),
162+
publisher_permissions_tx.clone(),
163+
)));
164+
140165
jhs.extend(network::spawn_network(
141166
config.clone(),
142167
network::Network::Secondary,
143168
state.clone(),
169+
publisher_permissions_rx,
144170
)?);
145171
}
146172

src/agent/metrics.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use {
22
super::state::local::PriceInfo,
3-
crate::agent::solana::oracle::PriceEntry,
3+
crate::agent::state::oracle::PriceEntry,
44
lazy_static::lazy_static,
55
prometheus_client::{
66
encoding::{

src/agent/services.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
pub mod keypairs;
22
pub mod notifier;
3+
pub mod oracle;
34

45
pub use {
56
keypairs::keypairs,
67
notifier::notifier,
8+
oracle::oracle,
79
};

src/agent/services/oracle.rs

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
//! Oracle
2+
//!
3+
//! The Oracle service is respoinsible for reacting to all remote/on-chain events.
4+
5+
use {
6+
crate::agent::{
7+
solana::{
8+
key_store::KeyStore,
9+
network::{
10+
Config,
11+
Network,
12+
},
13+
},
14+
state::oracle::{
15+
Oracle,
16+
PricePublishingMetadata,
17+
},
18+
},
19+
anyhow::Result,
20+
solana_account_decoder::UiAccountEncoding,
21+
solana_client::{
22+
nonblocking::{
23+
pubsub_client::PubsubClient,
24+
rpc_client::RpcClient,
25+
},
26+
rpc_config::{
27+
RpcAccountInfoConfig,
28+
RpcProgramAccountsConfig,
29+
},
30+
},
31+
solana_sdk::{
32+
account::Account,
33+
commitment_config::CommitmentConfig,
34+
pubkey::Pubkey,
35+
},
36+
std::{
37+
collections::HashMap,
38+
sync::Arc,
39+
time::{
40+
Duration,
41+
Instant,
42+
},
43+
},
44+
tokio::sync::watch::Sender,
45+
tokio_stream::StreamExt,
46+
};
47+
48+
pub async fn oracle<S>(
49+
config: Config,
50+
network: Network,
51+
state: Arc<S>,
52+
publisher_permissions_tx: Sender<HashMap<Pubkey, HashMap<Pubkey, PricePublishingMetadata>>>,
53+
) where
54+
S: Oracle,
55+
S: Send + Sync + 'static,
56+
{
57+
let Ok(key_store) = KeyStore::new(config.key_store.clone()) else {
58+
tracing::warn!("Key store not available, Oracle won't start.");
59+
return;
60+
};
61+
62+
tokio::spawn(poller(
63+
config.clone(),
64+
network,
65+
state.clone(),
66+
key_store.mapping_key,
67+
config.oracle.max_lookup_batch_size,
68+
publisher_permissions_tx.clone(),
69+
));
70+
71+
if config.oracle.subscriber_enabled {
72+
tokio::spawn(async move {
73+
loop {
74+
let current_time = Instant::now();
75+
if let Err(ref err) = subscriber(
76+
config.clone(),
77+
network,
78+
state.clone(),
79+
key_store.program_key,
80+
)
81+
.await
82+
{
83+
tracing::error!(err = ?err, "Subscriber exited unexpectedly.");
84+
if current_time.elapsed() < Duration::from_secs(30) {
85+
tracing::warn!("Subscriber restarting too quickly. Sleeping for 1 second.");
86+
tokio::time::sleep(Duration::from_secs(1)).await;
87+
}
88+
}
89+
}
90+
});
91+
}
92+
}
93+
94+
/// When an account RPC Subscription update is receiveed.
95+
///
96+
/// We check if the account is one we're aware of and tracking, and if so, spawn
97+
/// a small background task that handles that update. We only do this for price
98+
/// accounts, all other accounts are handled below in the poller.
99+
async fn subscriber<S>(
100+
config: Config,
101+
network: Network,
102+
state: Arc<S>,
103+
program_key: Pubkey,
104+
) -> Result<()>
105+
where
106+
S: Oracle,
107+
S: Send + Sync + 'static,
108+
{
109+
// Setup PubsubClient to listen for account changes on the Oracle program.
110+
let client = PubsubClient::new(config.wss_url.as_str()).await?;
111+
112+
let (mut notifier, _unsub) = {
113+
let program_key = program_key;
114+
let commitment = config.oracle.commitment;
115+
let config = RpcProgramAccountsConfig {
116+
account_config: RpcAccountInfoConfig {
117+
commitment: Some(CommitmentConfig { commitment }),
118+
encoding: Some(UiAccountEncoding::Base64Zstd),
119+
..Default::default()
120+
},
121+
filters: None,
122+
with_context: Some(true),
123+
};
124+
client.program_subscribe(&program_key, Some(config)).await
125+
}?;
126+
127+
while let Some(update) = notifier.next().await {
128+
match update.value.account.decode::<Account>() {
129+
Some(account) => {
130+
let pubkey: Pubkey = update.value.pubkey.as_str().try_into()?;
131+
let state = state.clone();
132+
tokio::spawn(async move {
133+
if let Err(err) =
134+
Oracle::handle_price_account_update(&*state, network, &pubkey, &account)
135+
.await
136+
{
137+
tracing::error!(err = ?err, "Failed to handle account update.");
138+
}
139+
});
140+
}
141+
142+
None => {
143+
tracing::error!(
144+
update = ?update,
145+
"Failed to decode account from update.",
146+
);
147+
}
148+
}
149+
}
150+
151+
tracing::debug!("Subscriber closed connection.");
152+
return Ok(());
153+
}
154+
155+
/// On poll lookup all Pyth Mapping/Product/Price accounts and sync.
156+
async fn poller<S>(
157+
config: Config,
158+
network: Network,
159+
state: Arc<S>,
160+
mapping_key: Pubkey,
161+
max_lookup_batch_size: usize,
162+
publisher_permissions_tx: Sender<HashMap<Pubkey, HashMap<Pubkey, PricePublishingMetadata>>>,
163+
) where
164+
S: Oracle,
165+
S: Send + Sync + 'static,
166+
{
167+
// Setup an RpcClient for manual polling.
168+
let mut tick = tokio::time::interval(config.oracle.poll_interval_duration);
169+
let client = Arc::new(RpcClient::new_with_timeout_and_commitment(
170+
config.rpc_url,
171+
config.rpc_timeout,
172+
CommitmentConfig {
173+
commitment: config.oracle.commitment,
174+
},
175+
));
176+
177+
loop {
178+
tick.tick().await;
179+
tracing::debug!("Polling for updates.");
180+
if let Err(err) = async {
181+
Oracle::poll_updates(
182+
&*state,
183+
mapping_key,
184+
&client,
185+
max_lookup_batch_size,
186+
publisher_permissions_tx.clone(),
187+
)
188+
.await?;
189+
Oracle::sync_global_store(&*state, network).await
190+
}
191+
.await
192+
{
193+
tracing::error!(err = ?err, "Failed to handle poll updates.");
194+
}
195+
}
196+
}

src/agent/solana.rs

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
pub mod exporter;
2-
pub mod oracle;
32

43
/// This module encapsulates all the interaction with a single Solana network:
54
/// - The Oracle, which reads data from the network
@@ -12,15 +11,22 @@ pub mod network {
1211
self,
1312
KeyStore,
1413
},
15-
oracle,
1614
},
17-
crate::agent::state::State,
15+
crate::agent::state::{
16+
oracle::{
17+
self,
18+
PricePublishingMetadata,
19+
},
20+
State,
21+
},
1822
anyhow::Result,
1923
serde::{
2024
Deserialize,
2125
Serialize,
2226
},
27+
solana_sdk::pubkey::Pubkey,
2328
std::{
29+
collections::HashMap,
2430
sync::Arc,
2531
time::Duration,
2632
},
@@ -70,25 +76,51 @@ pub mod network {
7076
pub exporter: exporter::Config,
7177
}
7278

79+
/// Spawn an Oracle, in-progress porting this to State.
80+
///
81+
/// Behaviour:
82+
/// - Spawns Oracle: (Obsolete, now Extracted to state/oracle.rs)
83+
/// - Spawns a Subscriber:
84+
/// o Subscribes to the Oracle program key.
85+
/// o Decodes account events related to the Oracle.
86+
/// o Sends update.
87+
/// - Spawns a Poller:
88+
/// o Fetches Mapping Accounts
89+
/// o Iterates Product+Price Accounts
90+
/// o Sends update.
91+
/// - Oracle then Listens for Updates from Subscriber
92+
/// o Filters for Price Account Updates.
93+
/// o Stores its own copy of the Price Account.
94+
/// o Updates the Global Store for that Price Account.
95+
/// - Oracle also Listens for Updates from Poller
96+
/// o Tracks if any new Mapping Accounts were found.
97+
/// o Update Local Data
98+
/// o Updates entire Global Store View.
99+
/// - Spawns Exporter:
100+
/// - Spawns NetworkQuerier
101+
/// - Queries BlockHash in a timer.
102+
/// - Sends BlockHash + Slot
103+
/// - Spawns Transaction Monitor:
104+
/// - Listens for for Transactions
105+
/// - Adds to tracked Transactions
106+
/// - Responds to queries about Tx status.
107+
/// - Spawns Exporter
108+
/// - On Publish tick: pushes updates to the network as a batch.
109+
/// - On Compute Unit Price Tick: calculates new median price fee from recent
110+
///
111+
/// Plan:
112+
/// - Subscriber & Poller Can Be Spawnable Tasks
113+
/// - Oracle becomes a State API
114+
/// -
73115
pub fn spawn_network(
74116
config: Config,
75117
network: Network,
76118
state: Arc<State>,
119+
publisher_permissions_rx: watch::Receiver<
120+
HashMap<Pubkey, HashMap<Pubkey, PricePublishingMetadata>>,
121+
>,
77122
) -> Result<Vec<JoinHandle<()>>> {
78-
// Publisher permissions updates between oracle and exporter
79-
let (publisher_permissions_tx, publisher_permissions_rx) = watch::channel(<_>::default());
80-
81-
// Spawn the Oracle
82-
let mut jhs = oracle::spawn_oracle(
83-
config.oracle.clone(),
84-
network,
85-
&config.rpc_url,
86-
&config.wss_url,
87-
config.rpc_timeout,
88-
publisher_permissions_tx,
89-
KeyStore::new(config.key_store.clone())?,
90-
state.clone(),
91-
);
123+
let mut jhs = vec![];
92124

93125
// Spawn the Exporter
94126
let exporter_jhs = exporter::spawn_exporter(
@@ -108,7 +140,7 @@ pub mod network {
108140
}
109141

110142
/// The key_store module is responsible for parsing the pythd key store.
111-
mod key_store {
143+
pub mod key_store {
112144
use {
113145
anyhow::Result,
114146
serde::{

src/agent/solana/exporter.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use {
33
super::{
44
key_store,
55
network::Network,
6-
oracle::PricePublishingMetadata,
76
},
87
crate::agent::state::{
98
global::GlobalStore,
@@ -12,6 +11,7 @@ use {
1211
LocalStore,
1312
PriceInfo,
1413
},
14+
oracle::PricePublishingMetadata,
1515
State,
1616
},
1717
anyhow::{
@@ -514,6 +514,7 @@ impl Exporter {
514514
/// (n / batch_size) requests in flight.
515515
async fn publish_updates(&mut self) -> Result<()> {
516516
let permissioned_updates = self.get_permissioned_updates().await?;
517+
let current_timestamp_millis = Utc::now().timestamp_millis();
517518

518519
if permissioned_updates.is_empty() {
519520
return Ok(());

0 commit comments

Comments
 (0)