Просмотр исходного кода

p2w-client: Add lib target, make helpers into lib functions there

commit-id:3aeb9ee6
Stan Drozd 3 лет назад
Родитель
Сommit
6d9725f99c

+ 7 - 21
solana/pyth2wormhole/Cargo.lock

@@ -392,19 +392,6 @@ dependencies = [
  "syn 1.0.73",
 ]
 
-[[package]]
-name = "console"
-version = "0.14.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45"
-dependencies = [
- "encode_unicode",
- "lazy_static",
- "libc",
- "terminal_size",
- "winapi",
-]
-
 [[package]]
 name = "console"
 version = "0.15.0"
@@ -603,7 +590,7 @@ version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "61579ada4ec0c6031cfac3f86fdba0d195a7ebeb5e36693bd53cb5999a25beeb"
 dependencies = [
- "console 0.15.0",
+ "console",
  "lazy_static",
  "tempfile",
  "zeroize",
@@ -1223,7 +1210,7 @@ version = "0.16.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2d207dc617c7a380ab07ff572a6e52fa202a2a8f355860ac9c38e23f8196be1b"
 dependencies = [
- "console 0.14.1",
+ "console",
  "lazy_static",
  "number_prefix",
  "regex",
@@ -2651,7 +2638,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a03587d5bf5f7bc9302385f9ada8412662cdb93b5e3d40fee2a02553a932277c"
 dependencies = [
  "base32",
- "console 0.15.0",
+ "console",
  "dialoguer",
  "hidapi",
  "log",
@@ -3127,11 +3114,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
 
 [[package]]
 name = "tokio"
-version = "1.7.1"
+version = "1.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fb2ed024293bb19f7a5dc54fe83bf86532a44c12a2bb8ba40d64a4509395ca2"
+checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a"
 dependencies = [
- "autocfg",
  "bytes",
  "libc",
  "memchr",
@@ -3147,9 +3133,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-macros"
-version = "1.2.0"
+version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37"
+checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
 dependencies = [
  "proc-macro2 1.0.27",
  "quote 1.0.9",

+ 5 - 1
solana/pyth2wormhole/client/Cargo.toml

@@ -5,6 +5,10 @@ edition = "2018"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
+[lib]
+name = "pyth2wormhole_client"
+src = "src/lib.rs"
+
 [features]
 default = ["pyth2wormhole/client", "wormhole-bridge-solana/client"]
 
@@ -23,4 +27,4 @@ solana-program = "=1.9.4"
 solana-sdk = "=1.9.4"
 solana-transaction-status = "=1.9.4"
 solitaire-client = {path = "../../solitaire/client"}
-solitaire = {path = "../../solitaire/program"}
+solitaire = {path = "../../solitaire/program"}

+ 2 - 1
solana/pyth2wormhole/client/src/attestation_cfg.rs

@@ -8,7 +8,6 @@ use serde::{
     Serializer,
 };
 use solana_program::pubkey::Pubkey;
-use solitaire::ErrBox;
 
 /// Pyth2wormhole config specific to attestation requests
 #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
@@ -56,6 +55,8 @@ where
 mod tests {
     use super::*;
 
+    use solitaire::ErrBox;
+
     #[test]
     fn test_sanity() -> Result<(), ErrBox> {
         let initial = AttestationConfig {

+ 3 - 3
solana/pyth2wormhole/client/src/cli.rs

@@ -50,9 +50,9 @@ pub enum Action {
         #[clap(short = 'f', long = "--config", about = "Attestation YAML config")]
         attestation_cfg: PathBuf,
     },
-    #[clap(
-        about = "Update an existing pyth2wormhole program's settings (currently set owner only)"
-    )]
+    #[clap(about = "Retrieve a pyth2wormhole program's current settings")]
+    GetConfig,
+    #[clap(about = "Update an existing pyth2wormhole program's settings")]
     SetConfig {
         /// Current owner keypair path
         #[clap(long = "owner", default_value = "~/.config/solana/id.json")]

+ 253 - 0
solana/pyth2wormhole/client/src/lib.rs

@@ -0,0 +1,253 @@
+pub mod attestation_cfg;
+
+use borsh::{
+    BorshDeserialize,
+    BorshSerialize,
+};
+use solana_client::rpc_client::RpcClient;
+use solana_program::{
+    hash::Hash,
+    instruction::{
+        AccountMeta,
+        Instruction,
+    },
+    pubkey::Pubkey,
+    system_program,
+    sysvar::{
+        clock,
+        rent,
+    },
+};
+use solana_sdk::transaction::Transaction;
+use solitaire::{
+    processors::seeded::Seeded,
+    AccountState,
+    ErrBox,
+};
+use solitaire_client::{
+    AccEntry,
+    Keypair,
+    SolSigner,
+    ToInstruction,
+};
+
+use bridge::{
+    accounts::{
+        Bridge,
+        FeeCollector,
+        Sequence,
+        SequenceDerivationData,
+    },
+    types::ConsistencyLevel,
+};
+
+use pyth2wormhole::{
+    attest::{
+        P2WEmitter,
+        P2W_MAX_BATCH_SIZE,
+    },
+    config::P2WConfigAccount,
+    initialize::InitializeAccounts,
+    set_config::SetConfigAccounts,
+    AttestData,
+    Pyth2WormholeConfig,
+};
+
+pub use attestation_cfg::{
+    AttestationConfig,
+    P2WSymbol,
+};
+
+pub fn gen_init_tx(
+    payer: Keypair,
+    p2w_addr: Pubkey,
+    new_owner_addr: Pubkey,
+    wh_prog: Pubkey,
+    pyth_owner_addr: Pubkey,
+    latest_blockhash: Hash,
+) -> Result<Transaction, ErrBox> {
+    use AccEntry::*;
+
+    let payer_pubkey = payer.pubkey();
+
+    let accs = InitializeAccounts {
+        payer: Signer(payer),
+        new_config: Derived(p2w_addr),
+    };
+
+    let config = Pyth2WormholeConfig {
+        max_batch_size: P2W_MAX_BATCH_SIZE,
+        owner: new_owner_addr,
+        wh_prog: wh_prog,
+        pyth_owner: pyth_owner_addr,
+    };
+    let ix_data = (pyth2wormhole::instruction::Instruction::Initialize, config);
+
+    let (ix, signers) = accs.to_ix(p2w_addr, ix_data.try_to_vec()?.as_slice())?;
+
+    let tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>(
+        &[ix],
+        Some(&payer_pubkey),
+        signers.iter().collect::<Vec<_>>().as_ref(),
+        latest_blockhash,
+    );
+    Ok(tx_signed)
+}
+
+pub fn gen_set_config_tx(
+    payer: Keypair,
+    p2w_addr: Pubkey,
+    owner: Keypair,
+    new_owner_addr: Pubkey,
+    new_wh_prog: Pubkey,
+    new_pyth_owner_addr: Pubkey,
+    latest_blockhash: Hash,
+) -> Result<Transaction, ErrBox> {
+    use AccEntry::*;
+
+    let payer_pubkey = payer.pubkey();
+
+    let accs = SetConfigAccounts {
+        payer: Signer(payer),
+        current_owner: Signer(owner),
+        config: Derived(p2w_addr),
+    };
+
+    let config = Pyth2WormholeConfig {
+        max_batch_size: P2W_MAX_BATCH_SIZE,
+        owner: new_owner_addr,
+        wh_prog: new_wh_prog,
+        pyth_owner: new_pyth_owner_addr,
+    };
+    let ix_data = (pyth2wormhole::instruction::Instruction::SetConfig, config);
+
+    let (ix, signers) = accs.to_ix(p2w_addr, ix_data.try_to_vec()?.as_slice())?;
+
+    let tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>(
+        &[ix],
+        Some(&payer_pubkey),
+        signers.iter().collect::<Vec<_>>().as_ref(),
+        latest_blockhash,
+    );
+    Ok(tx_signed)
+}
+
+/// Get the current config account data for given p2w program address
+pub fn get_config_account(
+    rpc_client: &RpcClient,
+    p2w_addr: &Pubkey,
+) -> Result<Pyth2WormholeConfig, ErrBox> {
+    let p2w_config_addr = P2WConfigAccount::<{ AccountState::Initialized }>::key(None, p2w_addr);
+
+    let config = Pyth2WormholeConfig::try_from_slice(
+        rpc_client.get_account_data(&p2w_config_addr)?.as_slice(),
+    )?;
+
+    Ok(config)
+}
+
+/// Generate an Instruction for making the attest() contract
+/// call.
+pub fn gen_attest_tx(
+    p2w_addr: Pubkey,
+    p2w_config: &Pyth2WormholeConfig, // Must be fresh, not retrieved inside to keep side effects away
+    payer: &Keypair,
+    symbols: &[P2WSymbol],
+    wh_msg: &Keypair,
+    latest_blockhash: Hash,
+) -> Result<Transaction, ErrBox> {
+    let emitter_addr = P2WEmitter::key(None, &p2w_addr);
+
+    let seq_addr = Sequence::key(
+        &SequenceDerivationData {
+            emitter_key: &emitter_addr,
+        },
+        &p2w_config.wh_prog,
+    );
+
+    let p2w_config_addr = P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_addr);
+    if symbols.len() > p2w_config.max_batch_size as usize {
+        return Err(format!(
+            "Expected up to {} symbols for batch, {} were found",
+            p2w_config.max_batch_size,
+            symbols.len()
+        )
+        .into());
+    }
+    // Initial attest() accounts
+    let mut acc_metas = vec![
+        // payer
+        AccountMeta::new(payer.pubkey(), true),
+        // system_program
+        AccountMeta::new_readonly(system_program::id(), false),
+        // config
+        AccountMeta::new_readonly(p2w_config_addr, false),
+    ];
+
+    // Batch contents and padding if applicable
+    let mut padded_symbols = {
+        let mut not_padded: Vec<_> = symbols
+            .iter()
+            .map(|s| {
+                vec![
+                    AccountMeta::new_readonly(s.product_addr, false),
+                    AccountMeta::new_readonly(s.price_addr, false),
+                ]
+            })
+            .flatten()
+            .collect();
+
+        // Align to max batch size with null accounts
+        let mut padding_accounts =
+            vec![
+                AccountMeta::new_readonly(Pubkey::new_from_array([0u8; 32]), false);
+                2 * (p2w_config.max_batch_size as usize - symbols.len())
+            ];
+        not_padded.append(&mut padding_accounts);
+
+        not_padded
+    };
+
+    acc_metas.append(&mut padded_symbols);
+
+    // Continue with other pyth2wormhole accounts
+    let mut acc_metas_remainder = vec![
+        // clock
+        AccountMeta::new_readonly(clock::id(), false),
+        // wh_prog
+        AccountMeta::new_readonly(p2w_config.wh_prog, false),
+        // wh_bridge
+        AccountMeta::new(
+            Bridge::<{ AccountState::Initialized }>::key(None, &p2w_config.wh_prog),
+            false,
+        ),
+        // wh_message
+        AccountMeta::new(wh_msg.pubkey(), true),
+        // wh_emitter
+        AccountMeta::new_readonly(emitter_addr, false),
+        // wh_sequence
+        AccountMeta::new(seq_addr, false),
+        // wh_fee_collector
+        AccountMeta::new(FeeCollector::<'_>::key(None, &p2w_config.wh_prog), false),
+        AccountMeta::new_readonly(rent::id(), false),
+    ];
+
+    acc_metas.append(&mut acc_metas_remainder);
+
+    let ix_data = (
+        pyth2wormhole::instruction::Instruction::Attest,
+        AttestData {
+            consistency_level: ConsistencyLevel::Finalized,
+        },
+    );
+
+    let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas);
+
+    let tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>(
+        &[ix],
+        Some(&payer.pubkey()),
+        &vec![&payer, &wh_msg],
+        latest_blockhash,
+    );
+    Ok(tx_signed)
+}

+ 23 - 214
solana/pyth2wormhole/client/src/main.rs

@@ -1,57 +1,31 @@
-pub mod attestation_cfg;
 pub mod cli;
 
 use std::{
     fs::File,
-    path::{
-        Path,
-        PathBuf,
-    },
 };
 
-use borsh::{
-    BorshDeserialize,
-    BorshSerialize,
-};
 use clap::Clap;
 use log::{
     debug,
     error,
     info,
-    warn,
     LevelFilter,
 };
 use solana_client::rpc_client::RpcClient;
 use solana_program::{
-    hash::Hash,
-    instruction::{
-        AccountMeta,
-        Instruction,
-    },
     pubkey::Pubkey,
-    system_program,
-    sysvar::{
-        clock,
-        rent,
-    },
 };
 use solana_sdk::{
     commitment_config::CommitmentConfig,
     signature::read_keypair_file,
-    transaction::Transaction,
 };
 use solana_transaction_status::UiTransactionEncoding;
 use solitaire::{
     processors::seeded::Seeded,
-    AccountState,
-    Derive,
-    Info,
+    ErrBox,
 };
 use solitaire_client::{
-    AccEntry,
     Keypair,
-    SolSigner,
-    ToInstruction,
 };
 
 use cli::{
@@ -59,33 +33,14 @@ use cli::{
     Cli,
 };
 
-use bridge::{
-    accounts::{
-        Bridge,
-        FeeCollector,
-        Sequence,
-        SequenceDerivationData,
-    },
-    types::ConsistencyLevel,
-    CHAIN_ID_SOLANA,
-};
-
 use pyth2wormhole::{
     attest::{
         P2WEmitter,
-        P2W_MAX_BATCH_SIZE,
     },
-    config::P2WConfigAccount,
-    initialize::InitializeAccounts,
-    set_config::SetConfigAccounts,
-    types::PriceAttestation,
-    AttestData,
-    Pyth2WormholeConfig,
 };
 
-use crate::attestation_cfg::AttestationConfig;
 
-pub type ErrBox = Box<dyn std::error::Error>;
+use pyth2wormhole_client::*;
 
 pub const SEQNO_PREFIX: &'static str = "Program log: Sequence: ";
 
@@ -106,7 +61,7 @@ fn main() -> Result<(), ErrBox> {
             pyth_owner_addr,
             wh_prog,
         } => {
-            let tx = handle_init(
+            let tx = gen_init_tx(
                 payer,
                 p2w_addr,
                 owner_addr,
@@ -116,13 +71,16 @@ fn main() -> Result<(), ErrBox> {
             )?;
             rpc_client.send_and_confirm_transaction_with_spinner(&tx)?;
         }
+        Action::GetConfig => {
+            println!("{:?}", get_config_account(&rpc_client, &p2w_addr)?);
+        }
         Action::SetConfig {
             ref owner,
             new_owner_addr,
             new_wh_prog,
             new_pyth_owner_addr,
         } => {
-            let tx = handle_set_config(
+            let tx = gen_set_config_tx(
                 payer,
                 p2w_addr,
                 read_keypair_file(&*shellexpand::tilde(&owner))?,
@@ -146,81 +104,6 @@ fn main() -> Result<(), ErrBox> {
 
     Ok(())
 }
-
-fn handle_init(
-    payer: Keypair,
-    p2w_addr: Pubkey,
-    new_owner_addr: Pubkey,
-    wh_prog: Pubkey,
-    pyth_owner_addr: Pubkey,
-    latest_blockhash: Hash,
-) -> Result<Transaction, ErrBox> {
-    use AccEntry::*;
-
-    let payer_pubkey = payer.pubkey();
-
-    let accs = InitializeAccounts {
-        payer: Signer(payer),
-        new_config: Derived(p2w_addr),
-    };
-
-    let config = Pyth2WormholeConfig {
-        max_batch_size: P2W_MAX_BATCH_SIZE,
-        owner: new_owner_addr,
-        wh_prog: wh_prog,
-        pyth_owner: pyth_owner_addr,
-    };
-    let ix_data = (pyth2wormhole::instruction::Instruction::Initialize, config);
-
-    let (ix, signers) = accs.to_ix(p2w_addr, ix_data.try_to_vec()?.as_slice())?;
-
-    let tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>(
-        &[ix],
-        Some(&payer_pubkey),
-        signers.iter().collect::<Vec<_>>().as_ref(),
-        latest_blockhash,
-    );
-    Ok(tx_signed)
-}
-
-fn handle_set_config(
-    payer: Keypair,
-    p2w_addr: Pubkey,
-    owner: Keypair,
-    new_owner_addr: Pubkey,
-    new_wh_prog: Pubkey,
-    new_pyth_owner_addr: Pubkey,
-    latest_blockhash: Hash,
-) -> Result<Transaction, ErrBox> {
-    use AccEntry::*;
-
-    let payer_pubkey = payer.pubkey();
-
-    let accs = SetConfigAccounts {
-        payer: Signer(payer),
-        current_owner: Signer(owner),
-        config: Derived(p2w_addr),
-    };
-
-    let config = Pyth2WormholeConfig {
-        max_batch_size: P2W_MAX_BATCH_SIZE,
-        owner: new_owner_addr,
-        wh_prog: new_wh_prog,
-        pyth_owner: new_pyth_owner_addr,
-    };
-    let ix_data = (pyth2wormhole::instruction::Instruction::SetConfig, config);
-
-    let (ix, signers) = accs.to_ix(p2w_addr, ix_data.try_to_vec()?.as_slice())?;
-
-    let tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>(
-        &[ix],
-        Some(&payer_pubkey),
-        signers.iter().collect::<Vec<_>>().as_ref(),
-        latest_blockhash,
-    );
-    Ok(tx_signed)
-}
-
 fn handle_attest(
     rpc_client: &RpcClient, // Needed for reading Pyth account data
     payer: Keypair,
@@ -232,18 +115,7 @@ fn handle_attest(
 
     info!("Using emitter addr {}", emitter_addr);
 
-    let p2w_config_addr = P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_addr);
-
-    let config = Pyth2WormholeConfig::try_from_slice(
-        rpc_client.get_account_data(&p2w_config_addr)?.as_slice(),
-    )?;
-
-    let seq_addr = Sequence::key(
-        &SequenceDerivationData {
-            emitter_key: &emitter_addr,
-        },
-        &config.wh_prog,
-    );
+    let config = get_config_account(rpc_client, &p2w_addr)?;
 
     // Read the current max batch size from the contract's settings
     let max_batch_size = config.max_batch_size;
@@ -277,7 +149,6 @@ fn handle_attest(
         .enumerate()
     {
         let batch_no = idx + 1;
-        let sym_msg_keypair = Keypair::new();
         info!(
             "Batch {}/{} contents: {:?}",
             batch_no,
@@ -291,87 +162,19 @@ fn handle_attest(
                 .collect::<Vec<_>>()
         );
 
-        let mut sym_metas_vec: Vec<_> = symbols
-            .iter()
-            .map(|s| {
-                vec![
-                    AccountMeta::new_readonly(s.product_addr, false),
-                    AccountMeta::new_readonly(s.price_addr, false),
-                ]
-            })
-            .flatten()
-            .collect();
-
-        // Align to max batch size with null accounts
-        let mut blank_accounts =
-            vec![
-                AccountMeta::new_readonly(Pubkey::new_from_array([0u8; 32]), false);
-                2 * (max_batch_size as usize - symbols.len())
-            ];
-        sym_metas_vec.append(&mut blank_accounts);
-
-        // Arrange Attest accounts
-        let mut acc_metas = vec![
-            // payer
-            AccountMeta::new(payer.pubkey(), true),
-            // system_program
-            AccountMeta::new_readonly(system_program::id(), false),
-            // config
-            AccountMeta::new_readonly(p2w_config_addr, false),
-        ];
-
-        // Insert max_batch_size metas
-        acc_metas.append(&mut sym_metas_vec);
-
-        // Continue with other pyth2wormhole accounts
-        let mut acc_metas_remainder = vec![
-            // clock
-            AccountMeta::new_readonly(clock::id(), false),
-            // wh_prog
-            AccountMeta::new_readonly(config.wh_prog, false),
-            // wh_bridge
-            AccountMeta::new(
-                Bridge::<{ AccountState::Initialized }>::key(None, &config.wh_prog),
-                false,
-            ),
-            // wh_message
-            AccountMeta::new(sym_msg_keypair.pubkey(), true),
-            // wh_emitter
-            AccountMeta::new_readonly(emitter_addr, false),
-            // wh_sequence
-            AccountMeta::new(seq_addr, false),
-            // wh_fee_collector
-            AccountMeta::new(FeeCollector::<'_>::key(None, &config.wh_prog), false),
-            AccountMeta::new_readonly(rent::id(), false),
-        ];
-
-        acc_metas.append(&mut acc_metas_remainder);
-
-        let ix_data = (
-            pyth2wormhole::instruction::Instruction::Attest,
-            AttestData {
-                consistency_level: ConsistencyLevel::Finalized,
-            },
-        );
-
-        let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas);
-
         // Execute the transaction, obtain the resulting sequence
-        // number. The and_then() calls enforce error handling
-        // location near loop end.
+        // number. The and_then() calls enforce permissible error
+        // handling location near loop end.
         let res = rpc_client
             .get_latest_blockhash()
+            .map_err(|e| -> ErrBox { e.into() })
             .and_then(|latest_blockhash| {
-                let tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>(
-                    &[ix],
-                    Some(&payer.pubkey()),
-                    &vec![&payer, &sym_msg_keypair],
-                    latest_blockhash,
-                );
-                rpc_client.send_and_confirm_transaction_with_spinner(&tx_signed)
+		let tx_signed = gen_attest_tx(p2w_addr, &config, &payer, symbols, &Keypair::new(), latest_blockhash)?;
+
+                rpc_client.send_and_confirm_transaction_with_spinner(&tx_signed).map_err(|e| -> ErrBox { e.into() })
             })
-            .and_then(|sig| rpc_client.get_transaction(&sig, UiTransactionEncoding::Json))
-            .map_err(|e| -> ErrBox { e.into() })
+            .and_then(|sig| rpc_client.get_transaction(&sig, UiTransactionEncoding::Json).map_err(|e| -> ErrBox { e.into() })
+	    )
             .and_then(|this_tx| {
                 this_tx
                     .transaction
@@ -413,7 +216,13 @@ fn handle_attest(
     if errors.len() > 0 {
         let err_list = errors.join("\n");
 
-        Err(format!("{} of {} batches failed:\n{}", errors.len(), batch_count, err_list).into())
+        Err(format!(
+            "{} of {} batches failed:\n{}",
+            errors.len(),
+            batch_count,
+            err_list
+        )
+        .into())
     } else {
         Ok(())
     }

+ 1 - 0
solana/pyth2wormhole/program/Cargo.toml

@@ -28,3 +28,4 @@ wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"], optional =
 serde = { version = "1", optional = true}
 serde_derive = { version = "1", optional = true}
 serde_json = { version = "1", optional = true}
+

+ 1 - 0
solana/pyth2wormhole/program/src/config.rs

@@ -19,6 +19,7 @@ use solitaire::{
 };
 
 #[derive(Default, BorshDeserialize, BorshSerialize)]
+#[cfg_attr(feature = "client", derive(Debug))]
 pub struct Pyth2WormholeConfig {
     ///  Authority owning this contract
     pub owner: Pubkey,