Skip to content

Commit 163fa44

Browse files
author
Stanisław Drozd
authored
Drozdziak1/p2w client mapping crawl (#286)
* pyth2wormhole-client: Add a mapping crawling routine * pyth2wormhole-client: Add mapping_addr for attestation config * pyth2wormhole-client: cargo fmt
1 parent 48a5902 commit 163fa44

File tree

9 files changed

+195
-28
lines changed

9 files changed

+195
-28
lines changed

solana/pyth2wormhole/Cargo.lock

Lines changed: 34 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

solana/pyth2wormhole/client/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ log = "0.4.14"
1919
wormhole-bridge-solana = {git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.8.9"}
2020
pyth2wormhole = {path = "../program"}
2121
p2w-sdk = { path = "../../../third_party/pyth/p2w-sdk/rust", features=["solana"] }
22-
pyth-sdk-solana = "0.5.0"
22+
pyth-sdk-solana = "0.6.1"
2323
serde = "1"
2424
serde_yaml = "0.8"
2525
shellexpand = "2.1.0"

solana/pyth2wormhole/client/src/attestation_cfg.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ pub struct AttestationConfig {
1919
pub min_msg_reuse_interval_ms: u64,
2020
#[serde(default = "default_max_msg_accounts")]
2121
pub max_msg_accounts: u64,
22+
/// Optionally, we take a mapping account to add remaining symbols from a Pyth deployments. These symbols are processed under attestation conditions for the `default` symbol group.
23+
#[serde(
24+
deserialize_with = "opt_pubkey_string_de",
25+
serialize_with = "opt_pubkey_string_ser"
26+
)]
27+
pub mapping_addr: Option<Pubkey>,
2228
pub symbol_groups: Vec<SymbolGroup>,
2329
}
2430

@@ -116,6 +122,25 @@ where
116122
Ok(pubkey)
117123
}
118124

125+
fn opt_pubkey_string_ser<S>(k_opt: &Option<Pubkey>, ser: S) -> Result<S::Ok, S::Error>
126+
where
127+
S: Serializer,
128+
{
129+
let k_str_opt = k_opt.clone().map(|k| k.to_string());
130+
131+
Option::<String>::serialize(&k_str_opt, ser)
132+
}
133+
134+
fn opt_pubkey_string_de<'de, D>(de: D) -> Result<Option<Pubkey>, D::Error>
135+
where
136+
D: Deserializer<'de>,
137+
{
138+
match Option::<String>::deserialize(de)? {
139+
Some(k) => Ok(Some(Pubkey::from_str(&k).map_err(D::Error::custom)?)),
140+
None => Ok(None),
141+
}
142+
}
143+
119144
#[cfg(test)]
120145
mod tests {
121146
use super::*;
@@ -163,6 +188,7 @@ mod tests {
163188
let cfg = AttestationConfig {
164189
min_msg_reuse_interval_ms: 1000,
165190
max_msg_accounts: 100_000,
191+
mapping_addr: None,
166192
symbol_groups: vec![fastbois, slowbois],
167193
};
168194

solana/pyth2wormhole/client/src/lib.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ use borsh::{
77
BorshDeserialize,
88
BorshSerialize,
99
};
10+
use log::{
11+
debug,
12+
trace,
13+
};
14+
use pyth_sdk_solana::state::{
15+
load_mapping_account,
16+
load_price_account,
17+
load_product_account,
18+
};
1019
use solana_client::nonblocking::rpc_client::RpcClient;
1120
use solana_program::{
1221
hash::Hash,
@@ -44,6 +53,11 @@ use bridge::{
4453
types::ConsistencyLevel,
4554
};
4655

56+
use std::collections::{
57+
HashMap,
58+
HashSet,
59+
};
60+
4761
use p2w_sdk::P2WEmitter;
4862

4963
use pyth2wormhole::{
@@ -321,3 +335,72 @@ pub fn gen_attest_tx(
321335
);
322336
Ok(tx_signed)
323337
}
338+
339+
/// Enumerates all products and their prices in a Pyth mapping.
340+
/// Returns map of: product address => [price addresses]
341+
pub async fn crawl_pyth_mapping(
342+
rpc_client: &RpcClient,
343+
first_mapping_addr: &Pubkey,
344+
) -> Result<HashMap<Pubkey, HashSet<Pubkey>>, ErrBox> {
345+
let mut ret = HashMap::new();
346+
347+
let mut n_mappings = 1; // We assume the first one must be valid
348+
let mut n_products = 0;
349+
let mut n_prices = 0;
350+
351+
let mut mapping_addr = first_mapping_addr.clone();
352+
353+
// loop until the last non-zero MappingAccount.next account
354+
loop {
355+
let mapping_bytes = rpc_client.get_account_data(&mapping_addr).await?;
356+
357+
let mapping = load_mapping_account(&mapping_bytes)?;
358+
359+
// loop through all products in this mapping; filter out zeroed-out empty product slots
360+
for prod_addr in mapping.products.iter().filter(|p| *p != &Pubkey::default()) {
361+
let prod_bytes = rpc_client.get_account_data(prod_addr).await?;
362+
let prod = load_product_account(&prod_bytes)?;
363+
364+
let mut price_addr = prod.px_acc.clone();
365+
366+
// loop until the last non-zero PriceAccount.next account
367+
loop {
368+
let price_bytes = rpc_client.get_account_data(&price_addr).await?;
369+
let price = load_price_account(&price_bytes)?;
370+
371+
// Append to existing set or create a new map entry
372+
ret.entry(prod_addr.clone())
373+
.or_insert(HashSet::new())
374+
.insert(price_addr);
375+
376+
n_prices += 1;
377+
378+
if price.next == Pubkey::default() {
379+
trace!("Product {}: processed {} prices", prod_addr, n_prices);
380+
break;
381+
}
382+
price_addr = price.next.clone();
383+
}
384+
385+
n_products += 1;
386+
}
387+
trace!(
388+
"Mapping {}: processed {} products",
389+
mapping_addr,
390+
n_products
391+
);
392+
393+
// Traverse other mapping accounts if applicable
394+
if mapping.next == Pubkey::default() {
395+
break;
396+
}
397+
mapping_addr = mapping.next.clone();
398+
n_mappings += 1;
399+
}
400+
debug!(
401+
"Processed {} price(s) in {} product account(s), in {} mapping account(s)",
402+
n_prices, n_products, n_mappings
403+
);
404+
405+
Ok(ret)
406+
}

solana/pyth2wormhole/client/src/main.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use log::{
2929
use solana_client::{
3030
client_error::ClientError,
3131
nonblocking::rpc_client::RpcClient,
32-
rpc_config::RpcTransactionConfig
32+
rpc_config::RpcTransactionConfig,
3333
};
3434
use solana_program::pubkey::Pubkey;
3535
use solana_sdk::{
@@ -176,6 +176,11 @@ async fn main() -> Result<(), ErrBox> {
176176
let attestation_cfg: AttestationConfig =
177177
serde_yaml::from_reader(File::open(attestation_cfg)?)?;
178178

179+
if let Some(mapping_addr) = attestation_cfg.mapping_addr.as_ref() {
180+
let additional_accounts = crawl_pyth_mapping(&rpc_client, mapping_addr).await?;
181+
info!("Additional mapping accounts:\n{:#?}", additional_accounts);
182+
}
183+
179184
handle_attest(
180185
cli.rpc_url,
181186
Duration::from_millis(cli.rpc_interval_ms),
@@ -190,7 +195,7 @@ async fn main() -> Result<(), ErrBox> {
190195
)
191196
.await?;
192197
}
193-
Action::GetEmitter => unreachable!{}
198+
Action::GetEmitter => unreachable! {},
194199
}
195200

196201
Ok(())
@@ -267,7 +272,10 @@ async fn handle_attest(
267272
rpc_interval,
268273
));
269274

270-
let message_q_mtx = Arc::new(Mutex::new(P2WMessageQueue::new(Duration::from_millis(attestation_cfg.min_msg_reuse_interval_ms), attestation_cfg.max_msg_accounts as usize)));
275+
let message_q_mtx = Arc::new(Mutex::new(P2WMessageQueue::new(
276+
Duration::from_millis(attestation_cfg.min_msg_reuse_interval_ms),
277+
attestation_cfg.max_msg_accounts as usize,
278+
)));
271279

272280
// Create attestation scheduling routines; see attestation_sched_job() for details
273281
let mut attestation_sched_futs = batches.into_iter().map(|(batch_no, batch)| {
@@ -297,7 +305,11 @@ async fn handle_attest(
297305
let errors: Vec<_> = results
298306
.iter()
299307
.enumerate()
300-
.filter_map(|(idx, r)| r.as_ref().err().map(|e| format!("Error {}: {:#?}\n", idx + 1, e)))
308+
.filter_map(|(idx, r)| {
309+
r.as_ref()
310+
.err()
311+
.map(|e| format!("Error {}: {:#?}\n", idx + 1, e))
312+
})
301313
.collect();
302314

303315
if !errors.is_empty() {
@@ -417,13 +429,10 @@ async fn attestation_sched_job(
417429
let group_name4err_msg = batch.group_name.clone();
418430

419431
// We never get to error reporting in daemon mode, attach a map_err
420-
let job_with_err_msg = job.map_err(move |e| {
432+
let job_with_err_msg = job.map_err(move |e| {
421433
warn!(
422434
"Batch {}/{}, group {:?} ERR: {:#?}",
423-
batch_no4err_msg,
424-
batch_count4err_msg,
425-
group_name4err_msg,
426-
e
435+
batch_no4err_msg, batch_count4err_msg, group_name4err_msg, e
427436
);
428437
e
429438
});

solana/pyth2wormhole/client/src/message.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ impl P2WMessageQueue {
3737
Self {
3838
accounts: VecDeque::new(),
3939
grace_period,
40-
max_accounts
40+
max_accounts,
4141
}
4242
}
4343
/// Finds or creates an account with last_used at least grace_period in the past.
@@ -59,7 +59,11 @@ impl P2WMessageQueue {
5959

6060
// Make sure we're not going over the limit
6161
if self.accounts.len() >= self.max_accounts {
62-
return Err(format!("Max message queue size of {} reached.", self.max_accounts).into());
62+
return Err(format!(
63+
"Max message queue size of {} reached.",
64+
self.max_accounts
65+
)
66+
.into());
6367
}
6468

6569
debug!(
@@ -106,7 +110,7 @@ pub mod test {
106110

107111
std::thread::sleep(Duration::from_millis(600));
108112

109-
// Account 0 should be in front, enough time passed
113+
// Account 0 should be in front, enough time passed
110114
let acc3 = q.get_account()?;
111115

112116
assert_eq!(q.accounts.len(), 2);

third_party/pyth/p2w_autoattest.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,20 +169,25 @@ def find_and_log_seqnos(s):
169169

170170
res = conn.getresponse()
171171

172-
pyth_accounts = None
172+
publisher_state_map = {}
173173

174174
if res.getheader("Content-Type") == "application/json":
175-
pyth_accounts = json.load(res)
175+
publisher_state_map = json.load(res)
176176
else:
177177
logging.error("Bad Content type")
178178
sys.exit(1)
179179

180+
pyth_accounts = publisher_state_map["symbols"]
181+
180182
logging.info(
181183
f"Retrieved {len(pyth_accounts)} Pyth accounts from endpoint: {pyth_accounts}"
182184
)
183185

184-
cfg_yaml = """
186+
mapping_addr = publisher_state_map["mapping_addr"]
187+
188+
cfg_yaml = f"""
185189
---
190+
mapping_addr: {mapping_addr}
186191
symbol_groups:
187192
- group_name: fast_interval_only
188193
conditions:

0 commit comments

Comments
 (0)